dscf-core 0.3.3 → 0.3.4

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: 411df39f071d6148439e3e9b6ad6e9b67bf6f41a303f822d37becb0fa7f6b709
4
+ data.tar.gz: e8140291f06f4e6221ad9ab86920cfee9c2c978bb83deced1c32c7cae6a269c5
5
5
  SHA512:
6
- metadata.gz: 8d13fd5bb155bc6b292bcbe46c28e35d5e3be4b070cb95183b30cc7da0abaef0f955b000af3a2282138b5272f98a181569e45ae322c047e695cf229daba194ec
7
- data.tar.gz: 56540f927754630b63a813a573e81f7c84cc2d94f462851536dc04b59cf6ecb1c85ee32e3156d43263e0de6256cd986dedc160d7d13da0f123cf8818a3b6df7e
6
+ metadata.gz: 4377884adbc0f72a0055e8bba6507c37ebf576c113b2193a4cc7eae59fb900406b7ac28ebf6edb624d18fb8efdda8208ecad86aae59f17c58cf0881461ba7ff1
7
+ data.tar.gz: a4269a8c6f9aaf0bd08d256b171d23d7639a3fbd1e6a2dff4b0da254fa88b48b8bbe475f88a2e66ddf929fd252ea8b3962bd573778fa8fd97feccfe3527cfceb
@@ -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,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
@@ -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.4".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.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Asrat
@@ -435,11 +435,13 @@ files:
435
435
  - app/controllers/concerns/dscf/core/pagination.rb
436
436
  - app/controllers/concerns/dscf/core/reviewable_controller.rb
437
437
  - app/controllers/concerns/dscf/core/token_authenticatable.rb
438
+ - app/controllers/dscf/core/account_management_controller.rb
438
439
  - app/controllers/dscf/core/addresses_controller.rb
439
440
  - app/controllers/dscf/core/application_controller.rb
440
441
  - app/controllers/dscf/core/auth_controller.rb
441
442
  - app/controllers/dscf/core/business_types_controller.rb
442
443
  - app/controllers/dscf/core/businesses_controller.rb
444
+ - app/controllers/dscf/core/fayda_verifications_controller.rb
443
445
  - app/controllers/dscf/core/files_controller.rb
444
446
  - app/controllers/dscf/core/permissions_controller.rb
445
447
  - app/controllers/dscf/core/role_permissions_controller.rb
@@ -452,6 +454,7 @@ files:
452
454
  - app/mailers/dscf/core/application_mailer.rb
453
455
  - app/models/concerns/dscf/core/attachable.rb
454
456
  - app/models/concerns/dscf/core/auditable_model.rb
457
+ - app/models/concerns/dscf/core/notifiable.rb
455
458
  - app/models/concerns/dscf/core/reviewable_model.rb
456
459
  - app/models/concerns/dscf/core/user_authenticatable.rb
457
460
  - app/models/dscf/core/address.rb
@@ -461,6 +464,7 @@ files:
461
464
  - app/models/dscf/core/business_type.rb
462
465
  - app/models/dscf/core/document.rb
463
466
  - app/models/dscf/core/file_attachment.rb
467
+ - app/models/dscf/core/notification.rb
464
468
  - app/models/dscf/core/permission.rb
465
469
  - app/models/dscf/core/refresh_token.rb
466
470
  - app/models/dscf/core/review.rb
@@ -469,6 +473,7 @@ files:
469
473
  - app/models/dscf/core/user.rb
470
474
  - app/models/dscf/core/user_profile.rb
471
475
  - app/models/dscf/core/user_role.rb
476
+ - app/policies/dscf/core/account_management_policy.rb
472
477
  - app/policies/dscf/core/application_policy.rb
473
478
  - app/policies/dscf/core/business_policy.rb
474
479
  - app/policies/dscf/core/business_type_policy.rb
@@ -493,6 +498,7 @@ files:
493
498
  - app/services/dscf/core/file_storage.rb
494
499
  - app/services/dscf/core/file_storage/client.rb
495
500
  - app/services/dscf/core/file_storage/uploader.rb
501
+ - app/services/dscf/core/notification_service.rb
496
502
  - app/services/dscf/core/token_service.rb
497
503
  - config/initializers/jwt.rb
498
504
  - config/initializers/ransack.rb
@@ -516,6 +522,8 @@ files:
516
522
  - db/migrate/20260304000001_create_dscf_core_permissions.rb
517
523
  - db/migrate/20260304000002_create_dscf_core_role_permissions.rb
518
524
  - db/migrate/20260501000001_add_is_system_to_dscf_core_businesses.rb
525
+ - db/migrate/20260616000000_create_dscf_core_notifications.rb
526
+ - db/migrate/20260616000001_add_status_to_dscf_core_users.rb
519
527
  - lib/dscf/core.rb
520
528
  - lib/dscf/core/engine.rb
521
529
  - lib/dscf/core/permission_registry.rb
@@ -532,6 +540,7 @@ files:
532
540
  - spec/factories/dscf/core/business_types.rb
533
541
  - spec/factories/dscf/core/businesses.rb
534
542
  - spec/factories/dscf/core/documents.rb
543
+ - spec/factories/dscf/core/notifications.rb
535
544
  - spec/factories/dscf/core/permissions.rb
536
545
  - spec/factories/dscf/core/refresh_tokens.rb
537
546
  - spec/factories/dscf/core/reviews.rb