Skip to content

Commit

Permalink
feat(dunning): Add update_payment_status for adyen payments (#2522)
Browse files Browse the repository at this point in the history
## Roadmap Task

👉
https://getlago.canny.io/feature-requests/p/send-reminders-for-overdue-invoices

 ## Context

We want to be able to manually request payment of the overdue balance
and send emails for reminders.

 ## Description

The goal of this change is to update the status of adyen payments for
payment requests.
  • Loading branch information
ancorcruz authored Sep 2, 2024
1 parent 86b00a6 commit a5e599d
Show file tree
Hide file tree
Showing 2 changed files with 195 additions and 14 deletions.
70 changes: 56 additions & 14 deletions app/services/payment_requests/payments/adyen_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ def create
payment_provider_customer_id: customer.adyen_customer.id,
amount_cents: payable.total_amount_cents,
amount_currency: payable.currency.upcase,
provider_payment_id: res.response['pspReference'],
status: res.response['resultCode']
provider_payment_id: res.response["pspReference"],
status: res.response["resultCode"]
)

ActiveRecord::Base.transaction do
Expand All @@ -67,7 +67,7 @@ def generate_payment_url
adyen_success, adyen_error = handle_adyen_response(result_url)
return result.service_failure!(code: adyen_error.code, message: adyen_error.msg) unless adyen_success

result.payment_url = result_url.response['url']
result.payment_url = result_url.response["url"]

result
rescue Adyen::AdyenError => e
Expand All @@ -76,6 +76,29 @@ def generate_payment_url
result.service_failure!(code: e.code, message: e.msg)
end

def update_payment_status(provider_payment_id:, status:, metadata: {})
payment = if metadata[:payment_type] == "one-time"
create_payment(provider_payment_id:, metadata:)
else
Payment.find_by(provider_payment_id:)
end
return result.not_found_failure!(resource: "adyen_payment") unless payment

result.payment = payment
result.payable = payment.payable
return result if payment.payable.payment_succeeded?

payment.update!(status:)

payable_payment_status = payable_payment_status(status)
update_payable_payment_status(payment_status: payable_payment_status)
update_invoices_payment_status(payment_status: payable_payment_status)

result
rescue BaseService::FailedResult => e
result.fail_with_error!(e)
end

private

attr_accessor :payable
Expand Down Expand Up @@ -106,7 +129,7 @@ def update_payment_method_id
Lago::Adyen::Params.new(payment_method_params).to_h
).response

payment_method_id = result['storedPaymentMethods']&.first&.dig('id')
payment_method_id = result["storedPaymentMethods"]&.first&.dig("id")
customer.adyen_customer.update!(payment_method_id:) if payment_method_id
end

Expand Down Expand Up @@ -139,13 +162,13 @@ def payment_params
},
reference: "Overdue invoices",
paymentMethod: {
type: 'scheme',
type: "scheme",
storedPaymentMethodId: customer.adyen_customer.payment_method_id
},
shopperReference: customer.adyen_customer.provider_customer_id,
merchantAccount: adyen_payment_provider.merchant_account,
shopperInteraction: 'ContAuth',
recurringProcessingModel: 'UnscheduledCardOnFile'
shopperInteraction: "ContAuth",
recurringProcessingModel: "UnscheduledCardOnFile"
}
prms[:shopperEmail] = customer.email if customer.email
prms
Expand All @@ -161,14 +184,14 @@ def payment_url_params
merchantAccount: adyen_payment_provider.merchant_account,
returnUrl: success_redirect_url,
shopperReference: customer.external_id,
storePaymentMethodMode: 'enabled',
recurringProcessingModel: 'UnscheduledCardOnFile',
storePaymentMethodMode: "enabled",
recurringProcessingModel: "UnscheduledCardOnFile",
expiresAt: Time.current + 70.days, # max link TTL
metadata: {
lago_customer_id: customer.id,
lago_payment_request_id: payable.id,
lago_invoice_ids: payable.invoice_ids,
payment_type: 'one-time'
payment_type: "one-time"
}
}
prms[:shopperEmail] = customer.email if customer.email
Expand All @@ -188,10 +211,14 @@ def payable_payment_status(payment_status)
end

def update_payable_payment_status(payment_status:, deliver_webhook: true)
payable.update!(
payment_status:,
ready_for_payment_processing: payment_status.to_sym != :succeeded
)
UpdateService.call(
payable: result.payable,
params: {
payment_status:,
ready_for_payment_processing: payment_status.to_sym != :succeeded
},
webhook_notification: deliver_webhook
).raise_if_error!
end

def update_invoices_payment_status(payment_status:, deliver_webhook: true)
Expand All @@ -207,6 +234,21 @@ def update_invoices_payment_status(payment_status:, deliver_webhook: true)
end
end

def create_payment(provider_payment_id:, metadata:)
@payable = PaymentRequest.find(metadata[:lago_payment_request_id])

payable.increment_payment_attempts!

Payment.new(
payable:,
payment_provider_id: adyen_payment_provider.id,
payment_provider_customer_id: customer.adyen_customer.id,
amount_cents: payable.total_amount_cents,
amount_currency: payable.currency.upcase,
provider_payment_id:
)
end

def deliver_error_webhook(adyen_error)
DeliverErrorWebhookService.call_async(payable, {
provider_customer_id: customer.adyen_customer.provider_customer_id,
Expand Down
139 changes: 139 additions & 0 deletions spec/services/payment_requests/payments/adyen_service_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -359,4 +359,143 @@
expect(payment_method_params).to eq(params)
end
end

describe "#update_payment_status" do
let(:payment) do
create(
:payment,
payable: payment_request,
provider_payment_id:,
status: "Pending"
)
end

let(:provider_payment_id) { "ch_123456" }

before do
allow(SendWebhookJob).to receive(:perform_later)
allow(SegmentTrackJob).to receive(:perform_later)
payment
end

it "updates the payment, payment_request and invoices payment_status", :aggregate_failures do
result = adyen_service.update_payment_status(
provider_payment_id:,
status: "Authorised"
)

expect(result).to be_success
expect(result.payment.status).to eq("Authorised")

expect(result.payable.reload).to be_payment_succeeded
expect(result.payable.ready_for_payment_processing).to eq(false)

expect(invoice_1.reload).to be_payment_succeeded
expect(invoice_1.ready_for_payment_processing).to eq(false)
expect(invoice_2.reload).to be_payment_succeeded
expect(invoice_2.ready_for_payment_processing).to eq(false)
end

context "when status is failed" do
it "updates the payment, payment_request and invoices status", :aggregate_failures do
result = adyen_service.update_payment_status(
provider_payment_id:,
status: "Refused"
)

expect(result).to be_success
expect(result.payment.status).to eq("Refused")

expect(result.payable.reload).to be_payment_failed
expect(result.payable.ready_for_payment_processing).to eq(true)

expect(invoice_1.reload).to be_payment_failed
expect(invoice_1.ready_for_payment_processing).to eq(true)

expect(invoice_2.reload).to be_payment_failed
expect(invoice_2.ready_for_payment_processing).to eq(true)
end
end

context "when payment_request and invoices is already payment_succeeded" do
before do
payment_request.payment_succeeded!
invoice_1.payment_succeeded!
invoice_2.payment_succeeded!
end

it "does not update the status of invoices, payment_request and payment" do
expect {
adyen_service.update_payment_status(
provider_payment_id:,
status: %w[Authorised SentForSettle SettleScheduled Settled Refunded].sample
)
}.to not_change { invoice_1.reload.payment_status }
.and not_change { invoice_2.reload.payment_status }
.and not_change { payment_request.reload.payment_status }
.and not_change { payment.reload.status }

result = adyen_service.update_payment_status(
provider_payment_id:,
status: %w[Authorised SentForSettle SettleScheduled Settled Refunded].sample
)

expect(result).to be_success
end
end

context "with invalid status" do
let(:status) { "invalid-status" }

it "does not update the payment_status of payment_request, invoices and payment" do
expect {
adyen_service.update_payment_status(provider_payment_id:, status:)
}.to not_change { payment_request.reload.payment_status }
.and not_change { invoice_1.reload.payment_status }
.and not_change { invoice_2.reload.payment_status }
.and change { payment.reload.status }.to(status)
end

it "returns an error", :aggregate_failures do
result = adyen_service.update_payment_status(provider_payment_id:, status:)

expect(result).not_to be_success
expect(result.error).to be_a(BaseService::ValidationFailure)
expect(result.error.messages.keys).to include(:payment_status)
expect(result.error.messages[:payment_status]).to include("value_is_invalid")
end
end

context "when payment is not found and it is one time payment" do
let(:payment) { nil }

before do
adyen_payment_provider
adyen_customer
end

it "creates a payment and updates payment request and invoices payment status", :aggregate_failures do
result = adyen_service.update_payment_status(
provider_payment_id:,
status: "succeeded",
metadata: {
lago_payment_request_id: payment_request.id,
payment_type: "one-time"
}
)

expect(result).to be_success
expect(result.payment.status).to eq("succeeded")

expect(result.payable).to be_payment_succeeded
expect(result.payable.ready_for_payment_processing).to eq(false)

expect(invoice_1.reload).to be_payment_succeeded
expect(invoice_1.ready_for_payment_processing).to eq(false)

expect(invoice_2.reload).to be_payment_succeeded
expect(invoice_2.ready_for_payment_processing).to eq(false)
end
end
end
end

0 comments on commit a5e599d

Please sign in to comment.