dscf-banking 0.3.5 → 0.3.7
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/app/controllers/dscf/banking/accounts_controller.rb +12 -18
- data/app/controllers/dscf/banking/applications_controller.rb +2 -2
- data/app/controllers/dscf/banking/transactions_controller.rb +1 -1
- data/app/controllers/dscf/banking/virtual_account_products_controller.rb +4 -4
- data/app/controllers/dscf/banking/vouchers_controller.rb +133 -0
- data/app/models/dscf/banking/account.rb +4 -1
- data/app/models/dscf/banking/voucher.rb +76 -0
- data/app/models/dscf/banking/voucher_redemption.rb +27 -0
- data/app/serializers/dscf/banking/account_serializer.rb +3 -2
- data/app/serializers/dscf/banking/application_serializer.rb +1 -1
- data/app/serializers/dscf/banking/document_serializer.rb +3 -1
- data/app/serializers/dscf/banking/voucher_redemption_serializer.rb +10 -0
- data/app/serializers/dscf/banking/voucher_serializer.rb +20 -0
- data/app/services/dscf/banking/voucher_service.rb +317 -0
- data/config/routes.rb +2 -2
- data/db/migrate/20260210120000_create_dscf_banking_vouchers.rb +23 -0
- data/db/migrate/20260210120010_create_dscf_banking_voucher_redemptions.rb +16 -0
- data/db/seeds.rb +30 -12
- data/lib/dscf/banking/version.rb +1 -1
- data/spec/factories/dscf/banking/accounts.rb +2 -0
- data/spec/factories/dscf/banking/voucher_redemptions.rb +9 -0
- data/spec/factories/dscf/banking/vouchers.rb +14 -0
- metadata +14 -20
- data/config/initializers/review_callbacks.rb +0 -23
- data/config/initializers/review_extensions.rb +0 -36
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1d9aebfb57294bdeee3240fe4079565174e97a172b44aaf93c4d93f3be94573d
|
|
4
|
+
data.tar.gz: f7acfa8939c35b2ce39f99a787772712ee886be3ed89326247948edf6f4b8a9e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 714c5cdb80e79dea3fd95842f9ef2ddb5e1f39c49af9f08ffee40625157ec2147fb0676430316885f8c28534811982a48ada4c471aef0c91a338d9c3fc068b5a
|
|
7
|
+
data.tar.gz: b1b6079ab0ad3d70f2c9c1076f70b06f6cefe57c118a1fa001710319a29054808bede0b95dfada63702968a6310ca74190d7013444c72889c7a1eca8ac034255
|
|
@@ -30,18 +30,12 @@ module Dscf::Banking
|
|
|
30
30
|
|
|
31
31
|
def full_name
|
|
32
32
|
account = Dscf::Banking::Account.find_by(account_number: params[:id])
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
else
|
|
40
|
-
render json: { success: false, error: "User profile not found" }, status: :not_found
|
|
41
|
-
end
|
|
42
|
-
else
|
|
43
|
-
render json: { success: false, error: "Account not found" }, status: :not_found
|
|
44
|
-
end
|
|
33
|
+
return render json: { success: false, error: "Account not found" }, status: :not_found unless account
|
|
34
|
+
|
|
35
|
+
full_name = account.name.to_s.strip
|
|
36
|
+
full_name = "Unnamed" if full_name.empty?
|
|
37
|
+
|
|
38
|
+
render json: { success: true, data: { full_name: full_name } }
|
|
45
39
|
end
|
|
46
40
|
|
|
47
41
|
def show
|
|
@@ -231,29 +225,29 @@ module Dscf::Banking
|
|
|
231
225
|
|
|
232
226
|
def transactions
|
|
233
227
|
account = Dscf::Banking::Account.find(params[:id])
|
|
234
|
-
|
|
228
|
+
|
|
235
229
|
transactions = Dscf::Banking::Transaction
|
|
236
230
|
.where("account_id = ? OR debit_account_id = ? OR credit_account_id = ?", account.id, account.id, account.id)
|
|
237
231
|
.includes(:account, :transaction_type, :debit_account, :credit_account)
|
|
238
|
-
|
|
232
|
+
|
|
239
233
|
# Apply pagination if requested
|
|
240
234
|
page = params[:page].to_i
|
|
241
235
|
per_page = (params[:per_page] || 10).to_i
|
|
242
|
-
|
|
236
|
+
|
|
243
237
|
# Apply filters
|
|
244
238
|
transactions = transactions.where(status: params[:status]) if params[:status].present?
|
|
245
239
|
transactions = transactions.where(transaction_type_id: params[:transaction_type_id]) if params[:transaction_type_id].present?
|
|
246
|
-
|
|
240
|
+
|
|
247
241
|
# Apply ordering
|
|
248
242
|
order_column = %w[id reference_number amount currency status created_at updated_at].include?(params[:order_by]) ? params[:order_by] : "created_at"
|
|
249
243
|
order_direction = %w[asc desc].include?(params[:order_direction]) ? params[:order_direction] : "desc"
|
|
250
244
|
transactions = transactions.order("#{order_column} #{order_direction}")
|
|
251
|
-
|
|
245
|
+
|
|
252
246
|
if page.positive?
|
|
253
247
|
total_count = transactions.count
|
|
254
248
|
total_pages = (total_count.to_f / per_page).ceil
|
|
255
249
|
transactions = transactions.offset((page - 1) * per_page).limit(per_page)
|
|
256
|
-
|
|
250
|
+
|
|
257
251
|
render json: {
|
|
258
252
|
success: true,
|
|
259
253
|
message: "Account transactions retrieved successfully",
|
|
@@ -24,7 +24,7 @@ module Dscf::Banking
|
|
|
24
24
|
|
|
25
25
|
private
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
def application_data(application)
|
|
28
28
|
{
|
|
29
29
|
id: application.id,
|
|
30
30
|
application_number: application.application_number,
|
|
@@ -52,7 +52,7 @@ module Dscf::Banking
|
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
def eager_loaded_associations
|
|
55
|
-
[ :user, :virtual_account_product, :
|
|
55
|
+
[ :user, :virtual_account_product, :documents, reviews: { reviewed_by: :user_profile } ]
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
def allowed_order_columns
|
|
@@ -83,10 +83,10 @@ module Dscf::Banking
|
|
|
83
83
|
|
|
84
84
|
def default_serializer_includes
|
|
85
85
|
{
|
|
86
|
-
index: [
|
|
87
|
-
show: [
|
|
88
|
-
create: [
|
|
89
|
-
update: [
|
|
86
|
+
index: [:product_category, :created_by, :approved_by, reviews: { reviewed_by: :user_profile }],
|
|
87
|
+
show: [:product_category, :created_by, :approved_by, reviews: { reviewed_by: :user_profile }],
|
|
88
|
+
create: [:reviews],
|
|
89
|
+
update: [:product_category, :created_by, :approved_by, reviews: { reviewed_by: :user_profile }]
|
|
90
90
|
}
|
|
91
91
|
end
|
|
92
92
|
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
module Dscf
|
|
2
|
+
module Banking
|
|
3
|
+
class VouchersController < ApplicationController
|
|
4
|
+
include Dscf::Core::Common
|
|
5
|
+
|
|
6
|
+
def create
|
|
7
|
+
parent_account = Dscf::Banking::Account.find_by(account_number: model_params[:parent_account_number])
|
|
8
|
+
|
|
9
|
+
unless parent_account
|
|
10
|
+
return render_error(errors: "Parent account not found", status: :not_found)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
service = Dscf::Banking::VoucherService.new
|
|
14
|
+
service.generate_voucher(
|
|
15
|
+
parent_account: parent_account,
|
|
16
|
+
amount: model_params[:amount],
|
|
17
|
+
recipient_mobile: model_params[:recipient_mobile],
|
|
18
|
+
recipient_name: model_params[:recipient_name],
|
|
19
|
+
message: model_params[:message],
|
|
20
|
+
expires_at: model_params[:expires_at]
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
if service.success?
|
|
24
|
+
render_success(data: service.voucher, serializer_options: { include: default_serializer_includes[:default] }, status: :created)
|
|
25
|
+
else
|
|
26
|
+
render_error(errors: service.errors, status: :unprocessable_entity)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def redeem
|
|
31
|
+
service = Dscf::Banking::VoucherService.new
|
|
32
|
+
destination_account = nil
|
|
33
|
+
|
|
34
|
+
if redeem_params[:destination_account_number].present?
|
|
35
|
+
destination_account = Dscf::Banking::Account.find_by(account_number: redeem_params[:destination_account_number])
|
|
36
|
+
unless destination_account
|
|
37
|
+
return render_error(errors: "Destination account not found", status: :not_found)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
service.redeem_voucher(
|
|
42
|
+
code: redeem_params[:code] || params[:code],
|
|
43
|
+
amount: redeem_params[:amount],
|
|
44
|
+
recipient_mobile: redeem_params[:recipient_mobile],
|
|
45
|
+
destination_account: destination_account
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
if service.success?
|
|
49
|
+
render_success(
|
|
50
|
+
data: {
|
|
51
|
+
voucher: service.voucher,
|
|
52
|
+
redemption: service.redemption
|
|
53
|
+
},
|
|
54
|
+
serializer_options: {
|
|
55
|
+
voucher: { include: default_serializer_includes[:default] },
|
|
56
|
+
redemption: { include: [ :destination_account ] }
|
|
57
|
+
}
|
|
58
|
+
)
|
|
59
|
+
else
|
|
60
|
+
render_error(errors: service.errors, status: :unprocessable_entity)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def show
|
|
65
|
+
voucher = Dscf::Banking::Voucher.find_by(code: params[:id].to_s.strip.upcase)
|
|
66
|
+
return render_error(errors: "Voucher not found", status: :not_found) unless voucher
|
|
67
|
+
|
|
68
|
+
if voucher.active? && voucher.expired?
|
|
69
|
+
Dscf::Banking::VoucherService.new.expire_voucher(voucher)
|
|
70
|
+
voucher.reload
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
render_success(data: voucher, serializer_options: { include: default_serializer_includes[:default] })
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def cancel
|
|
77
|
+
service = Dscf::Banking::VoucherService.new
|
|
78
|
+
service.cancel_voucher(code: params[:code])
|
|
79
|
+
|
|
80
|
+
if service.success?
|
|
81
|
+
render_success(data: service.voucher, serializer_options: { include: default_serializer_includes[:default] })
|
|
82
|
+
else
|
|
83
|
+
render_error(errors: service.errors, status: :unprocessable_entity)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def account_vouchers
|
|
88
|
+
account = Dscf::Banking::Account.find(params[:account_id])
|
|
89
|
+
vouchers = account.vouchers.order(created_at: :desc)
|
|
90
|
+
|
|
91
|
+
render_success(data: vouchers, serializer_options: { include: default_serializer_includes[:default] })
|
|
92
|
+
rescue ActiveRecord::RecordNotFound
|
|
93
|
+
render_error(errors: "Account not found", status: :not_found)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def model_params
|
|
99
|
+
params.require(:voucher).permit(
|
|
100
|
+
:parent_account_number,
|
|
101
|
+
:amount,
|
|
102
|
+
:recipient_mobile,
|
|
103
|
+
:recipient_name,
|
|
104
|
+
:message,
|
|
105
|
+
:expires_at
|
|
106
|
+
)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def redeem_params
|
|
110
|
+
params.require(:redemption).permit(
|
|
111
|
+
:code,
|
|
112
|
+
:amount,
|
|
113
|
+
:recipient_mobile,
|
|
114
|
+
:destination_account_number
|
|
115
|
+
)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def eager_loaded_associations
|
|
119
|
+
[]
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def allowed_order_columns
|
|
123
|
+
%w[id code status amount remaining_amount recipient_mobile created_at updated_at]
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def default_serializer_includes
|
|
127
|
+
{
|
|
128
|
+
default: [ :parent_account, :redemptions ]
|
|
129
|
+
}
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -3,6 +3,9 @@ module Dscf::Banking
|
|
|
3
3
|
belongs_to :virtual_account_product, optional: true
|
|
4
4
|
belongs_to :application, optional: true
|
|
5
5
|
|
|
6
|
+
has_many :vouchers, class_name: "Dscf::Banking::Voucher", foreign_key: :parent_account_id, dependent: :restrict_with_error
|
|
7
|
+
has_many :voucher_redemptions, through: :vouchers, class_name: "Dscf::Banking::VoucherRedemption"
|
|
8
|
+
|
|
6
9
|
enum :status, {
|
|
7
10
|
draft: 0,
|
|
8
11
|
pending_activation: 1,
|
|
@@ -16,7 +19,7 @@ module Dscf::Banking
|
|
|
16
19
|
validates :name, presence: true
|
|
17
20
|
validates :currency, presence: true
|
|
18
21
|
validates :current_balance, :available_balance, :minimum_balance,
|
|
19
|
-
|
|
22
|
+
numericality: { greater_than_or_equal_to: 0 }
|
|
20
23
|
validates :system_account, inclusion: { in: [ true, false ] }
|
|
21
24
|
validate :system_account_associations
|
|
22
25
|
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
module Dscf
|
|
2
|
+
module Banking
|
|
3
|
+
class Voucher < ApplicationRecord
|
|
4
|
+
self.table_name = "dscf_banking_vouchers"
|
|
5
|
+
|
|
6
|
+
belongs_to :parent_account, class_name: "Dscf::Banking::Account"
|
|
7
|
+
has_many :redemptions, class_name: "Dscf::Banking::VoucherRedemption", dependent: :restrict_with_error
|
|
8
|
+
|
|
9
|
+
enum :status, {
|
|
10
|
+
pending: 0,
|
|
11
|
+
active: 1,
|
|
12
|
+
redeemed: 2,
|
|
13
|
+
expired: 3,
|
|
14
|
+
cancelled: 4
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
validates :parent_account, presence: true
|
|
18
|
+
validates :amount, presence: true, numericality: { greater_than: 0 }
|
|
19
|
+
validates :remaining_amount, presence: true, numericality: { greater_than_or_equal_to: 0 }
|
|
20
|
+
validates :currency, presence: true
|
|
21
|
+
validates :code, presence: true, uniqueness: { case_sensitive: false }
|
|
22
|
+
validates :recipient_mobile, presence: true
|
|
23
|
+
validate :remaining_amount_not_exceed_amount
|
|
24
|
+
validate :expires_at_in_future, if: -> { expires_at.present? }
|
|
25
|
+
|
|
26
|
+
before_validation :set_defaults, on: :create
|
|
27
|
+
before_validation :normalize_code, if: -> { code.present? }
|
|
28
|
+
|
|
29
|
+
scope :by_status, ->(status) { where(status: status) }
|
|
30
|
+
scope :active_now, -> { where(status: :active).where("expires_at IS NULL OR expires_at > ?", Time.current) }
|
|
31
|
+
scope :expired_now, -> { where(status: :active).where("expires_at IS NOT NULL AND expires_at <= ?", Time.current) }
|
|
32
|
+
|
|
33
|
+
def expired?
|
|
34
|
+
expires_at.present? && expires_at <= Time.current
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def fully_redeemed?
|
|
38
|
+
remaining_amount.to_f.zero?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def mark_expired!
|
|
42
|
+
update!(status: :expired) if expired? && !expired_status?
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def expired_status?
|
|
46
|
+
status == "expired"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def set_defaults
|
|
52
|
+
self.currency ||= parent_account&.currency
|
|
53
|
+
self.remaining_amount = amount if remaining_amount.nil? && amount
|
|
54
|
+
self.status ||= :pending
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def normalize_code
|
|
58
|
+
self.code = code.to_s.strip.upcase
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def remaining_amount_not_exceed_amount
|
|
62
|
+
return if amount.nil? || remaining_amount.nil?
|
|
63
|
+
|
|
64
|
+
if remaining_amount > amount
|
|
65
|
+
errors.add(:remaining_amount, "cannot exceed voucher amount")
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def expires_at_in_future
|
|
70
|
+
if expires_at <= Time.current
|
|
71
|
+
errors.add(:expires_at, "must be in the future")
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module Dscf
|
|
2
|
+
module Banking
|
|
3
|
+
class VoucherRedemption < ApplicationRecord
|
|
4
|
+
self.table_name = "dscf_banking_voucher_redemptions"
|
|
5
|
+
|
|
6
|
+
belongs_to :voucher, class_name: "Dscf::Banking::Voucher"
|
|
7
|
+
belongs_to :destination_account, class_name: "Dscf::Banking::Account", optional: true
|
|
8
|
+
|
|
9
|
+
validates :voucher, presence: true
|
|
10
|
+
validates :amount, presence: true, numericality: { greater_than: 0 }
|
|
11
|
+
validates :recipient_mobile, presence: true
|
|
12
|
+
validates :currency, presence: true
|
|
13
|
+
validates :redeemed_at, presence: true
|
|
14
|
+
|
|
15
|
+
before_validation :set_defaults, on: :create
|
|
16
|
+
|
|
17
|
+
scope :by_voucher, ->(voucher_id) { where(voucher_id: voucher_id) }
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def set_defaults
|
|
22
|
+
self.currency ||= voucher&.currency
|
|
23
|
+
self.recipient_mobile ||= voucher&.recipient_mobile
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
module Dscf::Banking
|
|
2
2
|
class AccountSerializer < ActiveModel::Serializer
|
|
3
3
|
attributes :id, :account_number, :name, :status, :activation_date, :closure_date,
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
:account_properties, :current_balance, :available_balance, :minimum_balance,
|
|
5
|
+
:currency, :active, :created_at, :updated_at
|
|
6
6
|
|
|
7
7
|
belongs_to :virtual_account_product, serializer: VirtualAccountProductSerializer
|
|
8
8
|
|
|
9
9
|
attribute :application_id
|
|
10
|
+
attribute :system_account
|
|
10
11
|
end
|
|
11
12
|
end
|
|
@@ -9,7 +9,7 @@ module Dscf::Banking
|
|
|
9
9
|
belongs_to :assigned_branch_manager, optional: true
|
|
10
10
|
has_one :account, serializer: AccountSerializer, if: :has_account?
|
|
11
11
|
has_many :reviews, serializer: Dscf::Core::ReviewSerializer
|
|
12
|
-
has_many :documents, serializer:
|
|
12
|
+
has_many :documents, serializer: DocumentSerializer
|
|
13
13
|
|
|
14
14
|
def has_account?
|
|
15
15
|
object.has_account?
|
|
@@ -2,6 +2,8 @@ module Dscf::Banking
|
|
|
2
2
|
class DocumentSerializer < ActiveModel::Serializer
|
|
3
3
|
attributes :id, :document_type, :is_verified, :verified_at, :metadata, :file_urls, :created_at, :updated_at
|
|
4
4
|
|
|
5
|
+
belongs_to :documentable, polymorphic: true
|
|
6
|
+
|
|
5
7
|
def file_urls
|
|
6
8
|
return [] unless object.files.attached?
|
|
7
9
|
|
|
@@ -14,7 +16,7 @@ module Dscf::Banking
|
|
|
14
16
|
object.files.map(&:url)
|
|
15
17
|
end
|
|
16
18
|
end
|
|
17
|
-
rescue
|
|
19
|
+
rescue
|
|
18
20
|
[]
|
|
19
21
|
end
|
|
20
22
|
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
module Dscf
|
|
2
|
+
module Banking
|
|
3
|
+
class VoucherRedemptionSerializer < ActiveModel::Serializer
|
|
4
|
+
attributes :id, :voucher_id, :destination_account_id, :amount, :currency,
|
|
5
|
+
:recipient_mobile, :redeemed_at, :created_at, :updated_at
|
|
6
|
+
|
|
7
|
+
belongs_to :destination_account, serializer: AccountSerializer
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Dscf
|
|
2
|
+
module Banking
|
|
3
|
+
class VoucherSerializer < ActiveModel::Serializer
|
|
4
|
+
attributes :id, :parent_account_id, :code, :status, :amount, :remaining_amount,
|
|
5
|
+
:currency, :recipient_mobile, :recipient_name, :message, :expires_at,
|
|
6
|
+
:redeemed_at, :created_at, :updated_at
|
|
7
|
+
|
|
8
|
+
attribute :amount do
|
|
9
|
+
object.amount.to_f
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
attribute :remaining_amount do
|
|
13
|
+
object.remaining_amount.to_f
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
belongs_to :parent_account, serializer: AccountSerializer
|
|
17
|
+
has_many :redemptions, serializer: VoucherRedemptionSerializer
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
module Dscf
|
|
2
|
+
module Banking
|
|
3
|
+
class VoucherService < BaseTransactionService
|
|
4
|
+
attr_reader :voucher, :redemption
|
|
5
|
+
|
|
6
|
+
def initialize
|
|
7
|
+
super
|
|
8
|
+
@voucher = nil
|
|
9
|
+
@redemption = nil
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def generate_voucher(parent_account:, amount:, recipient_mobile:, recipient_name: nil, message: nil, expires_at: nil)
|
|
13
|
+
amount = amount.to_f
|
|
14
|
+
return self unless validate_generate_params(parent_account, amount, recipient_mobile)
|
|
15
|
+
|
|
16
|
+
expires_at ||= default_expiration
|
|
17
|
+
|
|
18
|
+
settlement_account = find_or_create_voucher_settlement_account(parent_account.currency)
|
|
19
|
+
return self unless success?
|
|
20
|
+
|
|
21
|
+
voucher_code = generate_code
|
|
22
|
+
|
|
23
|
+
ActiveRecord::Base.transaction do
|
|
24
|
+
transaction = create_voucher_transaction(
|
|
25
|
+
debit_account: parent_account,
|
|
26
|
+
credit_account: settlement_account,
|
|
27
|
+
amount: amount,
|
|
28
|
+
description: "Voucher issuance #{voucher_code}"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
return self unless success?
|
|
32
|
+
|
|
33
|
+
@voucher = Dscf::Banking::Voucher.create(
|
|
34
|
+
parent_account: parent_account,
|
|
35
|
+
amount: amount,
|
|
36
|
+
remaining_amount: amount,
|
|
37
|
+
currency: parent_account.currency,
|
|
38
|
+
recipient_mobile: recipient_mobile,
|
|
39
|
+
recipient_name: recipient_name,
|
|
40
|
+
message: message,
|
|
41
|
+
expires_at: expires_at,
|
|
42
|
+
status: :active,
|
|
43
|
+
code: voucher_code
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
unless @voucher.persisted?
|
|
47
|
+
@voucher.errors.full_messages.each { |msg| add_error(msg) }
|
|
48
|
+
raise ActiveRecord::Rollback
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
transaction&.update(description: "Voucher issuance #{@voucher.code}")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
self
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def redeem_voucher(code:, amount:, recipient_mobile:, destination_account: nil)
|
|
58
|
+
amount = amount.to_f
|
|
59
|
+
code = code.to_s.strip.upcase
|
|
60
|
+
@voucher = Dscf::Banking::Voucher.find_by(code: code)
|
|
61
|
+
|
|
62
|
+
unless @voucher
|
|
63
|
+
add_error("Voucher not found")
|
|
64
|
+
return self
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
return self unless validate_redeem_params(amount, recipient_mobile)
|
|
68
|
+
return self unless validate_voucher_state(@voucher, amount, recipient_mobile)
|
|
69
|
+
return self unless ensure_voucher_settlement_funds(@voucher.currency, amount)
|
|
70
|
+
|
|
71
|
+
if destination_account
|
|
72
|
+
return self unless validate_account_active(destination_account, "Destination account")
|
|
73
|
+
unless destination_account.currency == @voucher.currency
|
|
74
|
+
add_error("Currency mismatch between voucher and destination account")
|
|
75
|
+
return self
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
ActiveRecord::Base.transaction do
|
|
80
|
+
@voucher.lock!
|
|
81
|
+
remaining_after = @voucher.remaining_amount.to_f - amount
|
|
82
|
+
|
|
83
|
+
@redemption = Dscf::Banking::VoucherRedemption.create(
|
|
84
|
+
voucher: @voucher,
|
|
85
|
+
amount: amount,
|
|
86
|
+
currency: @voucher.currency,
|
|
87
|
+
recipient_mobile: recipient_mobile,
|
|
88
|
+
destination_account: destination_account,
|
|
89
|
+
redeemed_at: Time.current
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
unless @redemption.persisted?
|
|
93
|
+
@redemption.errors.full_messages.each { |msg| add_error(msg) }
|
|
94
|
+
raise ActiveRecord::Rollback
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
update_voucher_balance_and_status(@voucher, remaining_after)
|
|
98
|
+
return self unless success?
|
|
99
|
+
|
|
100
|
+
if destination_account
|
|
101
|
+
create_voucher_transaction(
|
|
102
|
+
debit_account: find_or_create_voucher_settlement_account(@voucher.currency),
|
|
103
|
+
credit_account: destination_account,
|
|
104
|
+
amount: amount,
|
|
105
|
+
description: "Voucher redemption #{@voucher.code}"
|
|
106
|
+
)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
self
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def cancel_voucher(code:)
|
|
114
|
+
@voucher = Dscf::Banking::Voucher.find_by(code: code.to_s.strip.upcase)
|
|
115
|
+
|
|
116
|
+
unless @voucher
|
|
117
|
+
add_error("Voucher not found")
|
|
118
|
+
return self
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
if @voucher.redeemed? || @voucher.cancelled?
|
|
122
|
+
add_error("Voucher cannot be cancelled")
|
|
123
|
+
return self
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
ActiveRecord::Base.transaction do
|
|
127
|
+
@voucher.lock!
|
|
128
|
+
refundable = @voucher.remaining_amount.to_f
|
|
129
|
+
@voucher.update!(status: :cancelled, remaining_amount: 0)
|
|
130
|
+
|
|
131
|
+
if refundable.positive?
|
|
132
|
+
create_voucher_transaction(
|
|
133
|
+
debit_account: find_or_create_voucher_settlement_account(@voucher.currency),
|
|
134
|
+
credit_account: @voucher.parent_account,
|
|
135
|
+
amount: refundable,
|
|
136
|
+
description: "Voucher cancellation #{@voucher.code}"
|
|
137
|
+
)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
self
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def expire_voucher(voucher)
|
|
145
|
+
return self unless voucher
|
|
146
|
+
|
|
147
|
+
ActiveRecord::Base.transaction do
|
|
148
|
+
voucher.lock!
|
|
149
|
+
return self unless voucher.active?
|
|
150
|
+
return self unless voucher.expired?
|
|
151
|
+
|
|
152
|
+
refundable = voucher.remaining_amount.to_f
|
|
153
|
+
voucher.update!(status: :expired, remaining_amount: 0)
|
|
154
|
+
|
|
155
|
+
if refundable.positive?
|
|
156
|
+
create_voucher_transaction(
|
|
157
|
+
debit_account: find_or_create_voucher_settlement_account(voucher.currency),
|
|
158
|
+
credit_account: voucher.parent_account,
|
|
159
|
+
amount: refundable,
|
|
160
|
+
description: "Voucher expiration #{voucher.code}"
|
|
161
|
+
)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
self
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def expire_due_vouchers
|
|
169
|
+
Dscf::Banking::Voucher.active_now.find_each do |voucher|
|
|
170
|
+
expire_voucher(voucher) if voucher.expired?
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
self
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
private
|
|
177
|
+
|
|
178
|
+
def validate_generate_params(parent_account, amount, recipient_mobile)
|
|
179
|
+
return false unless validate_account_active(parent_account, "Parent account")
|
|
180
|
+
|
|
181
|
+
if amount <= 0
|
|
182
|
+
add_error("Voucher amount must be greater than zero")
|
|
183
|
+
return false
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
if recipient_mobile.to_s.strip.empty?
|
|
187
|
+
add_error("Recipient mobile is required")
|
|
188
|
+
return false
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
return false unless validate_sufficient_funds(parent_account, amount)
|
|
192
|
+
|
|
193
|
+
true
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def default_expiration
|
|
197
|
+
30.days.from_now
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def validate_redeem_params(amount, recipient_mobile)
|
|
201
|
+
if amount <= 0
|
|
202
|
+
add_error("Redemption amount must be greater than zero")
|
|
203
|
+
return false
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
if recipient_mobile.to_s.strip.empty?
|
|
207
|
+
add_error("Recipient mobile is required")
|
|
208
|
+
return false
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
true
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def validate_voucher_state(voucher, amount, recipient_mobile)
|
|
215
|
+
if voucher.cancelled?
|
|
216
|
+
add_error("Voucher is cancelled")
|
|
217
|
+
return false
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
if voucher.expired?
|
|
221
|
+
add_error("Voucher is expired")
|
|
222
|
+
return false
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
if voucher.redeemed?
|
|
226
|
+
add_error("Voucher already redeemed")
|
|
227
|
+
return false
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
if voucher.recipient_mobile != recipient_mobile
|
|
231
|
+
add_error("Recipient mobile does not match")
|
|
232
|
+
return false
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
if voucher.remaining_amount.to_f < amount
|
|
236
|
+
add_error("Insufficient voucher balance")
|
|
237
|
+
return false
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
true
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def ensure_voucher_settlement_funds(currency, amount)
|
|
244
|
+
settlement_account = find_or_create_voucher_settlement_account(currency)
|
|
245
|
+
return false unless settlement_account
|
|
246
|
+
|
|
247
|
+
if settlement_account.current_balance.to_f < amount
|
|
248
|
+
add_error("Insufficient voucher settlement funds")
|
|
249
|
+
return false
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
true
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def update_voucher_balance_and_status(voucher, remaining_after)
|
|
256
|
+
new_status = remaining_after.zero? ? :redeemed : :active
|
|
257
|
+
updates = { remaining_amount: remaining_after, status: new_status }
|
|
258
|
+
updates[:redeemed_at] = Time.current if remaining_after.zero?
|
|
259
|
+
|
|
260
|
+
unless voucher.update(updates)
|
|
261
|
+
voucher.errors.full_messages.each { |msg| add_error(msg) }
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def generate_code
|
|
266
|
+
loop do
|
|
267
|
+
code = "VCH#{SecureRandom.hex(6).upcase}"
|
|
268
|
+
break code unless Dscf::Banking::Voucher.exists?(code: code)
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def create_voucher_transaction(debit_account:, credit_account:, amount:, description:)
|
|
273
|
+
return unless debit_account && credit_account
|
|
274
|
+
|
|
275
|
+
transaction_type = find_or_create_transaction_type("VOUCHER")
|
|
276
|
+
unless transaction_type
|
|
277
|
+
raise ActiveRecord::Rollback
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
transaction = create_transaction(
|
|
281
|
+
account: debit_account,
|
|
282
|
+
transaction_type: transaction_type,
|
|
283
|
+
debit_account: debit_account,
|
|
284
|
+
credit_account: credit_account,
|
|
285
|
+
amount: amount,
|
|
286
|
+
currency: debit_account.currency,
|
|
287
|
+
description: description,
|
|
288
|
+
status: :pending
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
unless transaction
|
|
292
|
+
raise ActiveRecord::Rollback
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
unless process_transaction(transaction)
|
|
296
|
+
raise ActiveRecord::Rollback
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
transaction
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def find_or_create_voucher_settlement_account(currency)
|
|
303
|
+
account = Dscf::Banking::Account.system_accounts
|
|
304
|
+
.where(currency: currency)
|
|
305
|
+
.where("name LIKE ?", "%Voucher Settlement%")
|
|
306
|
+
.first
|
|
307
|
+
|
|
308
|
+
unless account
|
|
309
|
+
add_error("Voucher settlement account not found for currency #{currency}")
|
|
310
|
+
return nil
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
account
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
end
|
data/config/routes.rb
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
Dscf::Banking::Engine.routes.draw do
|
|
2
|
-
|
|
3
|
-
get "full_name", to: "accounts#full_name", on: :member
|
|
2
|
+
resources :accounts do
|
|
4
3
|
collection do
|
|
5
4
|
get :me
|
|
6
5
|
end
|
|
@@ -9,6 +8,7 @@ Dscf::Banking::Engine.routes.draw do
|
|
|
9
8
|
post :suspend
|
|
10
9
|
post :close
|
|
11
10
|
get :transactions
|
|
11
|
+
get :full_name
|
|
12
12
|
end
|
|
13
13
|
end
|
|
14
14
|
resources :product_categories
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
class CreateDscfBankingVouchers < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :dscf_banking_vouchers do |t|
|
|
4
|
+
t.references :parent_account, null: false, foreign_key: { to_table: :dscf_banking_accounts }
|
|
5
|
+
t.string :code, null: false
|
|
6
|
+
t.integer :status, null: false, default: 0
|
|
7
|
+
t.decimal :amount, precision: 20, scale: 4, null: false
|
|
8
|
+
t.decimal :remaining_amount, precision: 20, scale: 4, null: false
|
|
9
|
+
t.string :currency, null: false, default: "ETB"
|
|
10
|
+
t.string :recipient_mobile, null: false
|
|
11
|
+
t.string :recipient_name
|
|
12
|
+
t.text :message
|
|
13
|
+
t.datetime :expires_at
|
|
14
|
+
t.datetime :redeemed_at
|
|
15
|
+
|
|
16
|
+
t.timestamps
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
add_index :dscf_banking_vouchers, :code, unique: true
|
|
20
|
+
add_index :dscf_banking_vouchers, :status
|
|
21
|
+
add_index :dscf_banking_vouchers, :recipient_mobile
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
class CreateDscfBankingVoucherRedemptions < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :dscf_banking_voucher_redemptions do |t|
|
|
4
|
+
t.references :voucher, null: false, foreign_key: { to_table: :dscf_banking_vouchers }
|
|
5
|
+
t.references :destination_account, foreign_key: { to_table: :dscf_banking_accounts }
|
|
6
|
+
t.decimal :amount, precision: 20, scale: 4, null: false
|
|
7
|
+
t.string :currency, null: false, default: "ETB"
|
|
8
|
+
t.string :recipient_mobile, null: false
|
|
9
|
+
t.datetime :redeemed_at, null: false
|
|
10
|
+
|
|
11
|
+
t.timestamps
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
add_index :dscf_banking_voucher_redemptions, :recipient_mobile
|
|
15
|
+
end
|
|
16
|
+
end
|
data/db/seeds.rb
CHANGED
|
@@ -9,42 +9,42 @@
|
|
|
9
9
|
|
|
10
10
|
# Seed roles based on RBAC documentation
|
|
11
11
|
puts "Seeding roles..."
|
|
12
|
-
user_role = Role.find_or_create_by(name:
|
|
13
|
-
officer_role = Role.find_or_create_by(name:
|
|
14
|
-
manager_role = Role.find_or_create_by(name:
|
|
15
|
-
kyc_officer_role = Role.find_or_create_by(name:
|
|
16
|
-
branch_manager_role = Role.find_or_create_by(name:
|
|
12
|
+
user_role = Dscf::Core::Role.find_or_create_by(name: "User")
|
|
13
|
+
officer_role = Dscf::Core::Role.find_or_create_by(name: "Virtual Account Officer")
|
|
14
|
+
manager_role = Dscf::Core::Role.find_or_create_by(name: "Virtual Account Manager")
|
|
15
|
+
kyc_officer_role = Dscf::Core::Role.find_or_create_by(name: "KYC Officer")
|
|
16
|
+
branch_manager_role = Dscf::Core::Role.find_or_create_by(name: "Branch Manager")
|
|
17
17
|
puts "Roles seeded successfully."
|
|
18
18
|
|
|
19
19
|
# Seed users with roles
|
|
20
20
|
puts "Seeding users..."
|
|
21
21
|
# Create users and assign roles
|
|
22
|
-
user1 = User.create(name: "Abebe Kebede", email: "abebe@example.com", password: "SecurePassword123!")
|
|
22
|
+
user1 = Dscf::Core::User.create(name: "Abebe Kebede", email: "abebe@example.com", password: "SecurePassword123!")
|
|
23
23
|
user1.roles << user_role
|
|
24
24
|
puts "Created user: #{user1.name}"
|
|
25
25
|
|
|
26
|
-
user2 = User.create(name: "Tigist Haile", email: "tigist@example.com", password: "SecurePassword123!")
|
|
26
|
+
user2 = Dscf::Core::User.create(name: "Tigist Haile", email: "tigist@example.com", password: "SecurePassword123!")
|
|
27
27
|
user2.roles << officer_role
|
|
28
28
|
puts "Created user: #{user2.name}"
|
|
29
29
|
|
|
30
|
-
user3 = User.create(name: "Dawit Mekonnen", email: "dawit@example.com", password: "SecurePassword123!")
|
|
30
|
+
user3 = Dscf::Core::User.create(name: "Dawit Mekonnen", email: "dawit@example.com", password: "SecurePassword123!")
|
|
31
31
|
user3.roles << manager_role
|
|
32
32
|
puts "Created user: #{user3.name}"
|
|
33
33
|
|
|
34
34
|
# Additional users for testing
|
|
35
|
-
user4 = User.create(name: "Hirut Assefa", email: "hirut@example.com", password: "SecurePassword123!")
|
|
35
|
+
user4 = Dscf::Core::User.create(name: "Hirut Assefa", email: "hirut@example.com", password: "SecurePassword123!")
|
|
36
36
|
user4.roles << [ user_role, officer_role ] # User with multiple roles if supported
|
|
37
37
|
puts "Created user: #{user4.name}"
|
|
38
38
|
|
|
39
|
-
user5 = User.create(name: "Solomon Tesfaye", email: "solomon@example.com", password: "SecurePassword123!")
|
|
39
|
+
user5 = Dscf::Core::User.create(name: "Solomon Tesfaye", email: "solomon@example.com", password: "SecurePassword123!")
|
|
40
40
|
user5.roles << manager_role
|
|
41
41
|
puts "Created user: #{user5.name}"
|
|
42
42
|
|
|
43
|
-
user6 = User.create(name: "Mulugeta Bekele", email: "mulugeta@example.com", password: "SecurePassword123!")
|
|
43
|
+
user6 = Dscf::Core::User.create(name: "Mulugeta Bekele", email: "mulugeta@example.com", password: "SecurePassword123!")
|
|
44
44
|
user6.roles << kyc_officer_role
|
|
45
45
|
puts "Created user: #{user6.name}"
|
|
46
46
|
|
|
47
|
-
user7 = User.create(name: "Fikirte Alemayehu", email: "fikirte@example.com", password: "SecurePassword123!")
|
|
47
|
+
user7 = Dscf::Core::User.create(name: "Fikirte Alemayehu", email: "fikirte@example.com", password: "SecurePassword123!")
|
|
48
48
|
user7.roles << branch_manager_role
|
|
49
49
|
puts "Created user: #{user7.name}"
|
|
50
50
|
|
|
@@ -68,6 +68,12 @@ transfer_type = Dscf::Banking::TransactionType.find_or_create_by(code: "TRANSFER
|
|
|
68
68
|
end
|
|
69
69
|
puts "Created transaction type: #{transfer_type.name}"
|
|
70
70
|
|
|
71
|
+
voucher_type = Dscf::Banking::TransactionType.find_or_create_by(code: "VOUCHER") do |tt|
|
|
72
|
+
tt.name = "Voucher"
|
|
73
|
+
tt.description = "Voucher issuance and redemption transactions"
|
|
74
|
+
end
|
|
75
|
+
puts "Created transaction type: #{voucher_type.name}"
|
|
76
|
+
|
|
71
77
|
# Seed system accounts for transaction processing
|
|
72
78
|
puts "Seeding system accounts..."
|
|
73
79
|
system_deposit_account = Dscf::Banking::Account.find_or_create_by(
|
|
@@ -94,4 +100,16 @@ system_withdrawal_account = Dscf::Banking::Account.find_or_create_by(
|
|
|
94
100
|
end
|
|
95
101
|
puts "Created system withdrawal account: #{system_withdrawal_account.name}"
|
|
96
102
|
|
|
103
|
+
voucher_settlement_account = Dscf::Banking::Account.find_or_create_by(
|
|
104
|
+
name: "Voucher Settlement Account",
|
|
105
|
+
system_account: true
|
|
106
|
+
) do |account|
|
|
107
|
+
account.currency = "ETB"
|
|
108
|
+
account.status = :active
|
|
109
|
+
account.current_balance = 0
|
|
110
|
+
account.available_balance = 0
|
|
111
|
+
account.minimum_balance = 0
|
|
112
|
+
end
|
|
113
|
+
puts "Created voucher settlement account: #{voucher_settlement_account.name}"
|
|
114
|
+
|
|
97
115
|
puts "Seeding completed successfully."
|
data/lib/dscf/banking/version.rb
CHANGED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
FactoryBot.define do
|
|
2
|
+
factory :voucher_redemption, class: "Dscf::Banking::VoucherRedemption" do
|
|
3
|
+
association :voucher, factory: :voucher
|
|
4
|
+
amount { 25.0 }
|
|
5
|
+
currency { voucher.currency }
|
|
6
|
+
recipient_mobile { voucher.recipient_mobile }
|
|
7
|
+
redeemed_at { Time.current }
|
|
8
|
+
end
|
|
9
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
FactoryBot.define do
|
|
2
|
+
factory :voucher, class: "Dscf::Banking::Voucher" do
|
|
3
|
+
parent_account { create(:dscf_banking_account, :active, :with_balance) }
|
|
4
|
+
amount { 100.0 }
|
|
5
|
+
remaining_amount { amount }
|
|
6
|
+
currency { parent_account.currency }
|
|
7
|
+
recipient_mobile { "0912345678" }
|
|
8
|
+
recipient_name { "Voucher Recipient" }
|
|
9
|
+
message { "Enjoy your voucher" }
|
|
10
|
+
expires_at { 30.days.from_now }
|
|
11
|
+
status { :active }
|
|
12
|
+
sequence(:code) { |n| "VCH#{n.to_s.rjust(6, "0")}" }
|
|
13
|
+
end
|
|
14
|
+
end
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dscf-banking
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.7
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
|
-
-
|
|
7
|
+
- Asrat Efrem
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date:
|
|
10
|
+
date: 2026-02-11 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: rails
|
|
@@ -219,20 +219,6 @@ dependencies:
|
|
|
219
219
|
- - ">="
|
|
220
220
|
- !ruby/object:Gem::Version
|
|
221
221
|
version: '0'
|
|
222
|
-
- !ruby/object:Gem::Dependency
|
|
223
|
-
name: bullet
|
|
224
|
-
requirement: !ruby/object:Gem::Requirement
|
|
225
|
-
requirements:
|
|
226
|
-
- - ">="
|
|
227
|
-
- !ruby/object:Gem::Version
|
|
228
|
-
version: '0'
|
|
229
|
-
type: :development
|
|
230
|
-
prerelease: false
|
|
231
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
232
|
-
requirements:
|
|
233
|
-
- - ">="
|
|
234
|
-
- !ruby/object:Gem::Version
|
|
235
|
-
version: '0'
|
|
236
222
|
- !ruby/object:Gem::Dependency
|
|
237
223
|
name: database_cleaner-active_record
|
|
238
224
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -431,7 +417,7 @@ dependencies:
|
|
|
431
417
|
version: '0'
|
|
432
418
|
description: An engine which contains core models for Supply Chain Financing.
|
|
433
419
|
email:
|
|
434
|
-
-
|
|
420
|
+
- asratextras77@gmail.com
|
|
435
421
|
executables: []
|
|
436
422
|
extensions: []
|
|
437
423
|
extra_rdoc_files: []
|
|
@@ -451,6 +437,7 @@ files:
|
|
|
451
437
|
- app/controllers/dscf/banking/transaction_types_controller.rb
|
|
452
438
|
- app/controllers/dscf/banking/transactions_controller.rb
|
|
453
439
|
- app/controllers/dscf/banking/virtual_account_products_controller.rb
|
|
440
|
+
- app/controllers/dscf/banking/vouchers_controller.rb
|
|
454
441
|
- app/jobs/dscf/banking/application_job.rb
|
|
455
442
|
- app/mailers/dscf/banking/application_mailer.rb
|
|
456
443
|
- app/models/concerns/dscf/banking/auditable.rb
|
|
@@ -466,6 +453,8 @@ files:
|
|
|
466
453
|
- app/models/dscf/banking/transaction.rb
|
|
467
454
|
- app/models/dscf/banking/transaction_type.rb
|
|
468
455
|
- app/models/dscf/banking/virtual_account_product.rb
|
|
456
|
+
- app/models/dscf/banking/voucher.rb
|
|
457
|
+
- app/models/dscf/banking/voucher_redemption.rb
|
|
469
458
|
- app/serializers/dscf/banking/account_serializer.rb
|
|
470
459
|
- app/serializers/dscf/banking/application_serializer.rb
|
|
471
460
|
- app/serializers/dscf/banking/document_serializer.rb
|
|
@@ -477,13 +466,14 @@ files:
|
|
|
477
466
|
- app/serializers/dscf/banking/transaction_serializer.rb
|
|
478
467
|
- app/serializers/dscf/banking/transaction_type_serializer.rb
|
|
479
468
|
- app/serializers/dscf/banking/virtual_account_product_serializer.rb
|
|
469
|
+
- app/serializers/dscf/banking/voucher_redemption_serializer.rb
|
|
470
|
+
- app/serializers/dscf/banking/voucher_serializer.rb
|
|
480
471
|
- app/services/dscf/banking/account_creation_service.rb
|
|
481
472
|
- app/services/dscf/banking/base_transaction_service.rb
|
|
482
473
|
- app/services/dscf/banking/deposit_service.rb
|
|
483
474
|
- app/services/dscf/banking/transfer_service.rb
|
|
475
|
+
- app/services/dscf/banking/voucher_service.rb
|
|
484
476
|
- app/services/dscf/banking/withdrawal_service.rb
|
|
485
|
-
- config/initializers/review_callbacks.rb
|
|
486
|
-
- config/initializers/review_extensions.rb
|
|
487
477
|
- config/locales/en.yml
|
|
488
478
|
- config/routes.rb
|
|
489
479
|
- db/migrate/20250830211002_create_dscf_banking_product_categories.rb
|
|
@@ -501,6 +491,8 @@ files:
|
|
|
501
491
|
- db/migrate/20250919084927_make_account_associations_optional.rb
|
|
502
492
|
- db/migrate/20250919182831_create_dscf_banking_transaction_types.rb
|
|
503
493
|
- db/migrate/20250919184220_create_dscf_banking_transactions.rb
|
|
494
|
+
- db/migrate/20260210120000_create_dscf_banking_vouchers.rb
|
|
495
|
+
- db/migrate/20260210120010_create_dscf_banking_voucher_redemptions.rb
|
|
504
496
|
- db/seeds.rb
|
|
505
497
|
- lib/dscf/banking.rb
|
|
506
498
|
- lib/dscf/banking/engine.rb
|
|
@@ -518,6 +510,8 @@ files:
|
|
|
518
510
|
- spec/factories/dscf/banking/transaction_types.rb
|
|
519
511
|
- spec/factories/dscf/banking/transactions.rb
|
|
520
512
|
- spec/factories/dscf/banking/virtual_account_products.rb
|
|
513
|
+
- spec/factories/dscf/banking/voucher_redemptions.rb
|
|
514
|
+
- spec/factories/dscf/banking/vouchers.rb
|
|
521
515
|
homepage: https://mksaddis.com/
|
|
522
516
|
licenses: []
|
|
523
517
|
metadata:
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
Rails.application.config.to_prepare do
|
|
2
|
-
ActiveSupport::Notifications.subscribe("review.created") do |name, start, finish, id, payload|
|
|
3
|
-
reviewable = payload[:reviewable]
|
|
4
|
-
|
|
5
|
-
if reviewable.is_a?(Dscf::Banking::Application)
|
|
6
|
-
reviewable.reload
|
|
7
|
-
review_status = reviewable.review_status
|
|
8
|
-
|
|
9
|
-
status_map = {
|
|
10
|
-
"submitted" => "submitted",
|
|
11
|
-
"under_review" => "under_review",
|
|
12
|
-
"approved" => "approved",
|
|
13
|
-
"rejected" => "rejected",
|
|
14
|
-
"request_modification" => "request_modification"
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
mapped_status = status_map[review_status]
|
|
18
|
-
if mapped_status && reviewable.status != mapped_status
|
|
19
|
-
reviewable.update!(status: mapped_status)
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
end
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
module Dscf::Banking
|
|
2
|
-
module ReviewExtensions
|
|
3
|
-
extend ActiveSupport::Concern
|
|
4
|
-
|
|
5
|
-
included do
|
|
6
|
-
after_create :sync_application_status, if: -> { reviewable.is_a?(Dscf::Banking::Application) }
|
|
7
|
-
end
|
|
8
|
-
|
|
9
|
-
private
|
|
10
|
-
|
|
11
|
-
def sync_application_status
|
|
12
|
-
return unless reviewable.is_a?(Dscf::Banking::Application)
|
|
13
|
-
|
|
14
|
-
application = reviewable
|
|
15
|
-
review_status = application.review_status
|
|
16
|
-
|
|
17
|
-
status_map = {
|
|
18
|
-
"submitted" => "submitted",
|
|
19
|
-
"under_review" => "under_review",
|
|
20
|
-
"approved" => "approved",
|
|
21
|
-
"rejected" => "rejected",
|
|
22
|
-
"request_modification" => "request_modification"
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
mapped_status = status_map[review_status]
|
|
26
|
-
if mapped_status && application.status != mapped_status
|
|
27
|
-
application.update!(status: mapped_status)
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
# Extend the Dscf::Core::Review model with our extensions
|
|
34
|
-
Rails.application.config.to_prepare do
|
|
35
|
-
Dscf::Core::Review.include(Dscf::Banking::ReviewExtensions) if defined?(Dscf::Core::Review)
|
|
36
|
-
end
|