dscf-payment 0.1.1
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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +28 -0
- data/Rakefile +8 -0
- data/app/controllers/dscf/payment/application_controller.rb +25 -0
- data/app/controllers/dscf/payment/payment_requests_controller.rb +70 -0
- data/app/controllers/dscf/payment/payments_controller.rb +32 -0
- data/app/jobs/dscf/payment/application_job.rb +6 -0
- data/app/mailers/dscf/payment/application_mailer.rb +8 -0
- data/app/models/dscf/payment/application_record.rb +7 -0
- data/app/models/dscf/payment/payment.rb +39 -0
- data/app/models/dscf/payment/payment_request.rb +64 -0
- data/app/serializers/dscf/payment/payment_request_serializer.rb +18 -0
- data/app/serializers/dscf/payment/payment_serializer.rb +16 -0
- data/app/services/dscf/payment/banking_integration_service.rb +37 -0
- data/app/services/dscf/payment/credit_integration_service.rb +56 -0
- data/app/services/dscf/payment/payment_service.rb +128 -0
- data/app/services/dscf/payment/settlement_account_service.rb +37 -0
- data/config/locales/en.yml +21 -0
- data/config/routes.rb +8 -0
- data/db/migrate/20251002191758_create_dscf_payment_payment_requests.rb +32 -0
- data/db/migrate/20251002191802_create_dscf_payment_payments.rb +24 -0
- data/db/migrate/20251003005136_make_payment_request_accounts_optional.rb +6 -0
- data/lib/dscf/payment/engine.rb +31 -0
- data/lib/dscf/payment/errors.rb +10 -0
- data/lib/dscf/payment/version.rb +5 -0
- data/lib/dscf/payment.rb +8 -0
- data/lib/tasks/dscf/payment_tasks.rake +4 -0
- data/spec/factories/dscf/payment/payment_requests.rb +56 -0
- data/spec/factories/dscf/payment/payments.rb +37 -0
- metadata +512 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 510fd8007681aac5a20434463cc546f4b9c108a7e5e53b8fa09c81f939267c6c
|
4
|
+
data.tar.gz: b0f00ba8049489d151673e54f2c5a73f4d3307750a2e65a84f969d5c537fcb43
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d87c868934387b69afceb4ed91f78ce1d5a3a7b808ca0d61fdc1201054f33409bcb50166c551d2b88e3acc946c510a9524bf72206a7a540cb4cd028e68ca7362
|
7
|
+
data.tar.gz: fb6b47a296c410f4d8ffdef7d2ed39c189458629f76c8c10010dd3755f2bcc6d489b8891bb2286c3e650705d10b9842ca9ce18a3f0c9f9d71f42ff462a476343
|
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::Payment
|
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-payment"
|
12
|
+
```
|
13
|
+
|
14
|
+
And then execute:
|
15
|
+
```bash
|
16
|
+
$ bundle
|
17
|
+
```
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
```bash
|
21
|
+
$ gem install dscf-payment
|
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,25 @@
|
|
1
|
+
module Dscf
|
2
|
+
module Payment
|
3
|
+
class ApplicationController < ActionController::API
|
4
|
+
include Dscf::Core::Authenticatable
|
5
|
+
include Dscf::Core::TokenAuthenticatable
|
6
|
+
include Dscf::Core::JsonResponse
|
7
|
+
|
8
|
+
# Handle CORS for authentication
|
9
|
+
before_action :set_cors_headers
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def set_cors_headers
|
14
|
+
headers["Access-Control-Allow-Origin"] = request.headers["Origin"] || "*"
|
15
|
+
headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, PATCH, DELETE, OPTIONS"
|
16
|
+
headers["Access-Control-Allow-Headers"] = "Origin, Content-Type, Accept, Authorization, X-Requested-With"
|
17
|
+
headers["Access-Control-Allow-Credentials"] = "false"
|
18
|
+
end
|
19
|
+
|
20
|
+
def authentication_required?
|
21
|
+
false # Override in specific controllers
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module Dscf::Payment
|
2
|
+
class PaymentRequestsController < ApplicationController
|
3
|
+
include Dscf::Core::Common
|
4
|
+
|
5
|
+
rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
|
6
|
+
|
7
|
+
def create
|
8
|
+
super do
|
9
|
+
Dscf::Payment::PaymentRequest.new(model_params)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def process_payment
|
14
|
+
payment_request = Dscf::Payment::PaymentRequest.find(params[:id])
|
15
|
+
|
16
|
+
unless payment_request.pending?
|
17
|
+
return render_error(
|
18
|
+
error: "Payment request cannot be processed",
|
19
|
+
errors: [ "Payment request status is #{payment_request.status}" ]
|
20
|
+
)
|
21
|
+
end
|
22
|
+
|
23
|
+
service = Dscf::Payment::PaymentService.new(payment_request, current_user)
|
24
|
+
payment = service.process
|
25
|
+
|
26
|
+
render_success(
|
27
|
+
data: payment,
|
28
|
+
message: "Payment processed successfully"
|
29
|
+
)
|
30
|
+
rescue ActiveRecord::RecordNotFound
|
31
|
+
render_error(error: "Payment request not found", status: :not_found)
|
32
|
+
rescue => e
|
33
|
+
render_error(error: "Payment processing failed", errors: [ e.message ])
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def authentication_required?
|
39
|
+
true
|
40
|
+
end
|
41
|
+
|
42
|
+
def model_params
|
43
|
+
params.require(:payment_request).permit(
|
44
|
+
:payment_type,
|
45
|
+
:amount,
|
46
|
+
:currency,
|
47
|
+
:description,
|
48
|
+
:from_account_id,
|
49
|
+
:to_account_id,
|
50
|
+
:payable_type,
|
51
|
+
:payable_id,
|
52
|
+
metadata: {}
|
53
|
+
)
|
54
|
+
end
|
55
|
+
|
56
|
+
def eager_loaded_associations
|
57
|
+
[ :from_account, :to_account, :payable, :payments ]
|
58
|
+
end
|
59
|
+
|
60
|
+
def allowed_order_columns
|
61
|
+
%w[id payment_type amount currency status reference_number created_at updated_at]
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def record_not_found
|
67
|
+
render_error(error: "Payment request not found", status: :not_found)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Dscf::Payment
|
2
|
+
class PaymentsController < ApplicationController
|
3
|
+
include Dscf::Core::Common
|
4
|
+
|
5
|
+
rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
|
6
|
+
|
7
|
+
private
|
8
|
+
|
9
|
+
def authentication_required?
|
10
|
+
true
|
11
|
+
end
|
12
|
+
|
13
|
+
def model_params
|
14
|
+
# Read-only controller - no creation via API
|
15
|
+
{}
|
16
|
+
end
|
17
|
+
|
18
|
+
def eager_loaded_associations
|
19
|
+
[ :payment_request, :banking_transaction ]
|
20
|
+
end
|
21
|
+
|
22
|
+
def allowed_order_columns
|
23
|
+
%w[id transaction_reference amount currency status processed_at created_at updated_at]
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def record_not_found
|
29
|
+
render_error(error: "Payment not found", status: :not_found)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Dscf::Payment
|
2
|
+
class Payment < ApplicationRecord
|
3
|
+
self.table_name = "dscf_payment_payments"
|
4
|
+
|
5
|
+
belongs_to :payment_request, class_name: "Dscf::Payment::PaymentRequest"
|
6
|
+
belongs_to :banking_transaction, class_name: "Dscf::Banking::Transaction", optional: true
|
7
|
+
|
8
|
+
validates :transaction_reference, :amount, presence: true
|
9
|
+
validates :amount, numericality: { greater_than: 0 }
|
10
|
+
validates :status, inclusion: { in: %w[pending processing completed failed cancelled] }
|
11
|
+
validates :transaction_reference, uniqueness: true
|
12
|
+
validates :currency, presence: true
|
13
|
+
|
14
|
+
before_validation :generate_transaction_reference, on: :create
|
15
|
+
|
16
|
+
scope :by_status, ->(status) { where(status: status) }
|
17
|
+
scope :completed, -> { where(status: "completed") }
|
18
|
+
scope :failed, -> { where(status: "failed") }
|
19
|
+
scope :recent, -> { order(created_at: :desc) }
|
20
|
+
|
21
|
+
def self.ransackable_attributes(auth_object = nil)
|
22
|
+
%w[id transaction_reference amount currency status processed_at failure_reason created_at updated_at]
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.ransackable_associations(auth_object = nil)
|
26
|
+
%w[payment_request banking_transaction]
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def generate_transaction_reference
|
32
|
+
return if transaction_reference.present?
|
33
|
+
|
34
|
+
timestamp = Time.current.strftime("%Y%m%d%H%M%S")
|
35
|
+
random_suffix = SecureRandom.hex(4).upcase
|
36
|
+
self.transaction_reference = "PAY#{timestamp}#{random_suffix}"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module Dscf::Payment
|
2
|
+
class PaymentRequest < ApplicationRecord
|
3
|
+
self.table_name = "dscf_payment_payment_requests"
|
4
|
+
|
5
|
+
belongs_to :from_account, class_name: "Dscf::Banking::Account", optional: true
|
6
|
+
belongs_to :to_account, class_name: "Dscf::Banking::Account", optional: true
|
7
|
+
belongs_to :payable, polymorphic: true, optional: true
|
8
|
+
has_many :payments, class_name: "Dscf::Payment::Payment", foreign_key: "payment_request_id", dependent: :destroy
|
9
|
+
|
10
|
+
validates :payment_type, :amount, :reference_number, presence: true
|
11
|
+
validates :amount, numericality: { greater_than: 0 }
|
12
|
+
validates :payment_type, inclusion: { in: %w[disbursement transfer repayment] }
|
13
|
+
validates :status, inclusion: { in: %w[pending approved processing completed failed cancelled] }
|
14
|
+
validates :reference_number, uniqueness: true
|
15
|
+
validates :currency, presence: true
|
16
|
+
|
17
|
+
before_validation :generate_reference_number, on: :create
|
18
|
+
|
19
|
+
scope :pending, -> { where(status: "pending") }
|
20
|
+
scope :approved, -> { where(status: "approved") }
|
21
|
+
scope :processing, -> { where(status: "processing") }
|
22
|
+
scope :completed, -> { where(status: "completed") }
|
23
|
+
scope :failed, -> { where(status: "failed") }
|
24
|
+
scope :by_type, ->(type) { where(payment_type: type) }
|
25
|
+
|
26
|
+
def pending?
|
27
|
+
status == "pending"
|
28
|
+
end
|
29
|
+
|
30
|
+
def approved?
|
31
|
+
status == "approved"
|
32
|
+
end
|
33
|
+
|
34
|
+
def processing?
|
35
|
+
status == "processing"
|
36
|
+
end
|
37
|
+
|
38
|
+
def completed?
|
39
|
+
status == "completed"
|
40
|
+
end
|
41
|
+
|
42
|
+
def failed?
|
43
|
+
status == "failed"
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.ransackable_attributes(auth_object = nil)
|
47
|
+
%w[id payment_type amount currency status reference_number description created_at updated_at]
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.ransackable_associations(auth_object = nil)
|
51
|
+
%w[from_account to_account payable payments]
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def generate_reference_number
|
57
|
+
return if reference_number.present?
|
58
|
+
|
59
|
+
timestamp = Time.current.strftime("%Y%m%d%H%M%S")
|
60
|
+
random_suffix = SecureRandom.hex(4).upcase
|
61
|
+
self.reference_number = "PAYREQ#{timestamp}#{random_suffix}"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Dscf::Payment
|
2
|
+
class PaymentRequestSerializer < ActiveModel::Serializer
|
3
|
+
attributes :id,
|
4
|
+
:payment_type,
|
5
|
+
:amount,
|
6
|
+
:currency,
|
7
|
+
:status,
|
8
|
+
:reference_number,
|
9
|
+
:description,
|
10
|
+
:created_at,
|
11
|
+
:updated_at
|
12
|
+
|
13
|
+
belongs_to :from_account, serializer: Dscf::Banking::AccountSerializer
|
14
|
+
belongs_to :to_account, serializer: Dscf::Banking::AccountSerializer
|
15
|
+
belongs_to :payable
|
16
|
+
has_many :payments, serializer: Dscf::Payment::PaymentSerializer
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Dscf::Payment
|
2
|
+
class PaymentSerializer < ActiveModel::Serializer
|
3
|
+
attributes :id,
|
4
|
+
:transaction_reference,
|
5
|
+
:amount,
|
6
|
+
:currency,
|
7
|
+
:status,
|
8
|
+
:processed_at,
|
9
|
+
:failure_reason,
|
10
|
+
:created_at,
|
11
|
+
:updated_at
|
12
|
+
|
13
|
+
belongs_to :payment_request, serializer: Dscf::Payment::PaymentRequestSerializer
|
14
|
+
belongs_to :banking_transaction, serializer: Dscf::Banking::TransactionSerializer
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Dscf::Payment
|
2
|
+
class BankingIntegrationService
|
3
|
+
def transfer(from_account, to_account, amount, description, transaction_type_code: "TRANSFER")
|
4
|
+
validate_transfer_params(from_account, to_account, amount)
|
5
|
+
|
6
|
+
transfer_service = Dscf::Banking::TransferService.new(
|
7
|
+
debit_account: from_account,
|
8
|
+
credit_account: to_account,
|
9
|
+
amount: amount,
|
10
|
+
description: description,
|
11
|
+
transaction_type_code: transaction_type_code
|
12
|
+
)
|
13
|
+
|
14
|
+
result = transfer_service.execute
|
15
|
+
|
16
|
+
unless result.success?
|
17
|
+
raise BankingTransactionError.new("Banking transfer failed: #{result.errors.join(", ")}")
|
18
|
+
end
|
19
|
+
|
20
|
+
result.transaction
|
21
|
+
rescue AccountNotActiveError, InvalidAmountError, InsufficientFundsError, BankingTransactionError
|
22
|
+
raise
|
23
|
+
rescue => e
|
24
|
+
raise BankingTransactionError.new("Banking transfer error: #{e.message}")
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def validate_transfer_params(from_account, to_account, amount)
|
30
|
+
raise InvalidAmountError.new("Amount must be positive") unless amount&.positive?
|
31
|
+
raise AccountNotActiveError.new("From account not active") unless from_account&.active? && from_account.status == "active"
|
32
|
+
raise AccountNotActiveError.new("To account not active") unless to_account&.active? && to_account.status == "active"
|
33
|
+
raise InsufficientFundsError.new("Insufficient funds") unless from_account.sufficient_funds_for_withdrawal?(amount)
|
34
|
+
raise BankingTransactionError.new("Currency mismatch") unless from_account.currency == to_account.currency
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Dscf::Payment
|
2
|
+
class CreditIntegrationService
|
3
|
+
def process_disbursement(amount, loan_profile, eligible_credit_line)
|
4
|
+
validate_disbursement_params(amount, loan_profile, eligible_credit_line)
|
5
|
+
|
6
|
+
disbursement_service = Dscf::Credit::DisbursementService.new(
|
7
|
+
amount: amount,
|
8
|
+
loan_profile: loan_profile,
|
9
|
+
eligible_credit_line: eligible_credit_line
|
10
|
+
)
|
11
|
+
|
12
|
+
result = disbursement_service.process_disbursement
|
13
|
+
|
14
|
+
unless result[:success]
|
15
|
+
raise CreditOperationError.new("Disbursement failed: #{result[:error]}")
|
16
|
+
end
|
17
|
+
|
18
|
+
result
|
19
|
+
rescue CreditOperationError, InvalidAmountError
|
20
|
+
raise
|
21
|
+
rescue => e
|
22
|
+
raise CreditOperationError.new("Credit disbursement error: #{e.message}")
|
23
|
+
end
|
24
|
+
|
25
|
+
def process_repayment(loan, amount, current_user)
|
26
|
+
validate_repayment_params(loan, amount, current_user)
|
27
|
+
|
28
|
+
repayment_service = Dscf::Credit::RepaymentService.new(loan, amount, current_user)
|
29
|
+
result = repayment_service.process_repayment
|
30
|
+
|
31
|
+
unless result[:success]
|
32
|
+
raise CreditOperationError.new("Repayment failed: #{result[:error]}")
|
33
|
+
end
|
34
|
+
|
35
|
+
result
|
36
|
+
rescue CreditOperationError, InvalidAmountError
|
37
|
+
raise
|
38
|
+
rescue => e
|
39
|
+
raise CreditOperationError.new("Credit repayment error: #{e.message}")
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def validate_disbursement_params(amount, loan_profile, eligible_credit_line)
|
45
|
+
raise InvalidAmountError.new("Amount must be positive") unless amount&.positive?
|
46
|
+
raise CreditOperationError.new("Loan profile required") unless loan_profile
|
47
|
+
raise CreditOperationError.new("Eligible credit line required") unless eligible_credit_line
|
48
|
+
end
|
49
|
+
|
50
|
+
def validate_repayment_params(loan, amount, current_user)
|
51
|
+
raise InvalidAmountError.new("Amount must be positive") unless amount&.positive?
|
52
|
+
raise CreditOperationError.new("Loan required") unless loan
|
53
|
+
raise CreditOperationError.new("Current user required") unless current_user
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
module Dscf::Payment
|
2
|
+
class PaymentService
|
3
|
+
attr_reader :payment_request, :current_user
|
4
|
+
|
5
|
+
def initialize(payment_request, current_user = nil)
|
6
|
+
@payment_request = payment_request
|
7
|
+
@current_user = current_user
|
8
|
+
end
|
9
|
+
|
10
|
+
def process
|
11
|
+
validate_payment_request
|
12
|
+
|
13
|
+
ActiveRecord::Base.transaction do
|
14
|
+
payment = create_payment_record
|
15
|
+
|
16
|
+
case payment_request.payment_type
|
17
|
+
when "disbursement"
|
18
|
+
process_disbursement(payment)
|
19
|
+
when "transfer"
|
20
|
+
process_transfer(payment)
|
21
|
+
when "repayment"
|
22
|
+
process_repayment(payment)
|
23
|
+
else
|
24
|
+
raise InvalidPaymentTypeError.new("Unknown payment type: #{payment_request.payment_type}")
|
25
|
+
end
|
26
|
+
|
27
|
+
mark_payment_completed(payment)
|
28
|
+
payment
|
29
|
+
end
|
30
|
+
rescue => e
|
31
|
+
handle_payment_failure(e)
|
32
|
+
raise
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def validate_payment_request
|
38
|
+
raise InvalidPaymentTypeError.new("Invalid payment type") unless valid_payment_type?
|
39
|
+
raise PaymentError.new("Payment request already processed") if payment_request.processing? || payment_request.completed?
|
40
|
+
end
|
41
|
+
|
42
|
+
def valid_payment_type?
|
43
|
+
%w[disbursement transfer repayment].include?(payment_request.payment_type)
|
44
|
+
end
|
45
|
+
|
46
|
+
def create_payment_record
|
47
|
+
payment_request.payments.create!(
|
48
|
+
amount: payment_request.amount,
|
49
|
+
currency: payment_request.currency,
|
50
|
+
status: "processing"
|
51
|
+
)
|
52
|
+
end
|
53
|
+
|
54
|
+
def process_disbursement(payment)
|
55
|
+
# For disbursements: settlement account -> customer account
|
56
|
+
settlement_account = SettlementAccountService.settlement_account_for(currency: payment_request.currency)
|
57
|
+
|
58
|
+
# First, process the credit disbursement
|
59
|
+
credit_service = CreditIntegrationService.new
|
60
|
+
credit_service.process_disbursement(
|
61
|
+
payment_request.amount,
|
62
|
+
payment_request.payable, # Should be loan_profile
|
63
|
+
payment_request.metadata["eligible_credit_line"] # Need to store this in metadata
|
64
|
+
)
|
65
|
+
|
66
|
+
# Then transfer from settlement to customer account
|
67
|
+
banking_service = BankingIntegrationService.new
|
68
|
+
banking_transaction = banking_service.transfer(
|
69
|
+
settlement_account,
|
70
|
+
payment_request.to_account,
|
71
|
+
payment_request.amount,
|
72
|
+
"Loan disbursement for #{payment_request.reference_number}"
|
73
|
+
)
|
74
|
+
|
75
|
+
payment.update!(banking_transaction: banking_transaction)
|
76
|
+
end
|
77
|
+
|
78
|
+
def process_transfer(payment)
|
79
|
+
# Direct account to account transfer
|
80
|
+
banking_service = BankingIntegrationService.new
|
81
|
+
banking_transaction = banking_service.transfer(
|
82
|
+
payment_request.from_account,
|
83
|
+
payment_request.to_account,
|
84
|
+
payment_request.amount,
|
85
|
+
"Account transfer for #{payment_request.reference_number}"
|
86
|
+
)
|
87
|
+
|
88
|
+
payment.update!(banking_transaction: banking_transaction)
|
89
|
+
end
|
90
|
+
|
91
|
+
def process_repayment(payment)
|
92
|
+
# For repayments: customer account -> settlement account
|
93
|
+
settlement_account = SettlementAccountService.settlement_account_for(currency: payment_request.currency)
|
94
|
+
|
95
|
+
# First transfer to settlement account
|
96
|
+
banking_service = BankingIntegrationService.new
|
97
|
+
banking_transaction = banking_service.transfer(
|
98
|
+
payment_request.from_account,
|
99
|
+
settlement_account,
|
100
|
+
payment_request.amount,
|
101
|
+
"Loan repayment for #{payment_request.reference_number}"
|
102
|
+
)
|
103
|
+
|
104
|
+
credit_service = CreditIntegrationService.new
|
105
|
+
credit_service.process_repayment(
|
106
|
+
payment_request.payable,
|
107
|
+
payment_request.amount,
|
108
|
+
current_user
|
109
|
+
)
|
110
|
+
|
111
|
+
payment.update!(banking_transaction: banking_transaction)
|
112
|
+
end
|
113
|
+
|
114
|
+
def mark_payment_completed(payment)
|
115
|
+
payment.update!(status: "completed", processed_at: Time.current)
|
116
|
+
payment_request.update!(status: "completed")
|
117
|
+
end
|
118
|
+
|
119
|
+
def handle_payment_failure(error)
|
120
|
+
payment_request.payments.where(status: "processing").update_all(
|
121
|
+
status: "failed",
|
122
|
+
failure_reason: error.message,
|
123
|
+
processed_at: Time.current
|
124
|
+
)
|
125
|
+
payment_request.update_column(:status, "failed")
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Dscf::Payment
|
2
|
+
class SettlementAccountService
|
3
|
+
SETTLEMENT_ACCOUNT_NAME = "DSCF Payment Settlement Account"
|
4
|
+
|
5
|
+
class << self
|
6
|
+
def find_or_create_settlement_account(currency: "ETB")
|
7
|
+
account = Dscf::Banking::Account.find_by(
|
8
|
+
name: SETTLEMENT_ACCOUNT_NAME,
|
9
|
+
currency: currency,
|
10
|
+
system_account: true
|
11
|
+
)
|
12
|
+
|
13
|
+
return account if account
|
14
|
+
|
15
|
+
# Create new settlement account
|
16
|
+
Dscf::Banking::Account.create!(
|
17
|
+
name: SETTLEMENT_ACCOUNT_NAME,
|
18
|
+
currency: currency,
|
19
|
+
system_account: true,
|
20
|
+
current_balance: 0,
|
21
|
+
available_balance: 0,
|
22
|
+
minimum_balance: 0,
|
23
|
+
status: :active,
|
24
|
+
active: true
|
25
|
+
)
|
26
|
+
rescue ActiveRecord::RecordInvalid => e
|
27
|
+
raise SettlementAccountError.new("Failed to create settlement account: #{e.message}")
|
28
|
+
end
|
29
|
+
|
30
|
+
def settlement_account_for(currency: "ETB")
|
31
|
+
account = find_or_create_settlement_account(currency: currency)
|
32
|
+
raise SettlementAccountError.new("Settlement account not found for currency #{currency}") unless account
|
33
|
+
account
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
en:
|
2
|
+
dscf:
|
3
|
+
payment:
|
4
|
+
payment_request:
|
5
|
+
success:
|
6
|
+
create: "Payment request created successfully"
|
7
|
+
show: "Payment request retrieved successfully"
|
8
|
+
index: "Payment requests retrieved successfully"
|
9
|
+
process: "Payment processed successfully"
|
10
|
+
errors:
|
11
|
+
create: "Failed to create payment request"
|
12
|
+
show: "Payment request not found"
|
13
|
+
index: "Failed to retrieve payment requests"
|
14
|
+
process: "Failed to process payment"
|
15
|
+
payment:
|
16
|
+
success:
|
17
|
+
show: "Payment retrieved successfully"
|
18
|
+
index: "Payments retrieved successfully"
|
19
|
+
errors:
|
20
|
+
show: "Payment not found"
|
21
|
+
index: "Failed to retrieve payments"
|