diff --git a/CHANGELOG.md b/CHANGELOG.md index da108e8e4..f046e958b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ - fix a bug: user was not able to reserve at the same time of an event with pre-registration invalidated - fix a bug: avoids crash if invoicing_profile has no address associated to it +- improvement: improves how pay_zen transactions are matched with payment_schedule_items +- improvement: improves rrule of pay_zen subscriptions ## v6.3.4 2023 November 23 diff --git a/lib/pay_zen/service.rb b/lib/pay_zen/service.rb index 1cbd73c9e..7e3938060 100644 --- a/lib/pay_zen/service.rb +++ b/lib/pay_zen/service.rb @@ -21,14 +21,15 @@ class PayZen::Service < Payment::Service amount: payzen_amount(first_item.details['recurring'].to_i), effect_date: first_item.due_date.iso8601, payment_method_token: token_id, - rrule: rrule(payment_schedule), + rrule: rrule(payment_schedule, first_item.due_date), order_id: order_id } initial_amount = first_item.amount initial_amount -= payment_schedule.wallet_amount if payment_schedule.wallet_amount if initial_amount.zero? - params[:effect_date] = (first_item.due_date + 1.month).iso8601 - params[:rrule] = rrule(payment_schedule, -1) + effect_date = first_item.due_date + 1.month + params[:effect_date] = effect_date.iso8601 + params[:rrule] = rrule(payment_schedule, effect_date, -1) else params[:initial_amount] = payzen_amount(initial_amount) params[:initial_amount_number] = 1 @@ -92,8 +93,8 @@ class PayZen::Service < Payment::Service return end pz_order = payment_schedule_item.payment_schedule.gateway_order.retrieve - transaction = pz_order['answer']['transactions'].last - return unless transaction_matches?(transaction, payment_schedule_item) + transaction = find_transaction_by_payment_schedule_item(pz_order['answer']['transactions'], payment_schedule_item) + return unless transaction case transaction['status'] when 'PAID' @@ -127,28 +128,35 @@ class PayZen::Service < Payment::Service amount end - private + def rrule(payment_schedule, first_date, offset = 0) + count = payment_schedule.payment_schedule_items.count + offset - def rrule(payment_schedule, offset = 0) - count = payment_schedule.payment_schedule_items.count - "RRULE:FREQ=MONTHLY;COUNT=#{count + offset}" - end - - # check if the given transaction matches the given PaymentScheduleItem - def transaction_matches?(transaction, payment_schedule_item) - transaction_date = Time.zone.parse(transaction['creationDate']).to_date - - amount = payment_schedule_item.amount - if payment_schedule_item == payment_schedule_item.payment_schedule.ordered_items.first && - payment_schedule_item.payment_schedule.wallet_amount - amount -= payment_schedule_item.payment_schedule.wallet_amount + by_month_day_part = case first_date.day + when 31 + "BYMONTHDAY=28,29,30,31;BYSETPOS=-1" + when 30 + "BYMONTHDAY=28,29,30;BYSETPOS=-1" + when 29 + "BYMONTHDAY=28,29;BYSETPOS=-1" + else + "BYMONTHDAY=#{first_date.day}" end - transaction['amount'] == amount && - transaction_date >= payment_schedule_item.due_date.to_date && - transaction_date <= payment_schedule_item.due_date.to_date + 7.days + "RRULE:FREQ=MONTHLY;#{by_month_day_part};COUNT=#{count}" end + def find_transaction_by_payment_schedule_item(transactions, payment_schedule_item) + due_date = payment_schedule_item.due_date.to_date + + transactions.find do |tr| + expected_capture_date = Time.zone.parse(tr["transactionDetails"]["paymentMethodDetails"]["expectedCaptureDate"]).to_date + + (tr["operationType"] == "DEBIT") && (expected_capture_date.between?(due_date - 1.day, due_date + 1.day)) + end + end + + private + # @see https://payzen.io/en-EN/payment-file/ips/list-of-supported-currencies.html def zero_decimal_currencies %w[KHR JPY KRW XOF XPF] diff --git a/test/lib/pay_zen/service_test.rb b/test/lib/pay_zen/service_test.rb new file mode 100644 index 000000000..52ce46349 --- /dev/null +++ b/test/lib/pay_zen/service_test.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +require 'test_helper' +require 'pay_zen/service' + +class PayZen::ServiceTest < ActiveSupport::TestCase + setup do + @service = PayZen::Service.new + end + + test '#rrule' do + ps = payment_schedules(:payment_schedule_12) + + first_due_date = ps.ordered_items.first.due_date + + first_date = first_due_date + assert_equal "RRULE:FREQ=MONTHLY;BYMONTHDAY=14;COUNT=12", @service.rrule(ps, first_date) + + assert_equal "RRULE:FREQ=MONTHLY;BYMONTHDAY=14;COUNT=11", @service.rrule(ps, first_date, -1) + + first_date = first_due_date + 3.days + assert_equal "RRULE:FREQ=MONTHLY;BYMONTHDAY=17;COUNT=12", @service.rrule(ps, first_date) + + first_due_date = first_due_date.change(month: 7) + + first_date = first_due_date.change(day: 28) + assert_equal "RRULE:FREQ=MONTHLY;BYMONTHDAY=28;COUNT=12", @service.rrule(ps, first_date) + + first_date = first_due_date.change(day: 29) + assert_equal "RRULE:FREQ=MONTHLY;BYMONTHDAY=28,29;BYSETPOS=-1;COUNT=12", @service.rrule(ps, first_date) + + first_date = first_due_date.change(day: 30) + assert_equal "RRULE:FREQ=MONTHLY;BYMONTHDAY=28,29,30;BYSETPOS=-1;COUNT=12", @service.rrule(ps, first_date) + + first_date = first_due_date.change(day: 31) + assert_equal "RRULE:FREQ=MONTHLY;BYMONTHDAY=28,29,30,31;BYSETPOS=-1;COUNT=12", @service.rrule(ps, first_date) + end + + def format_transaction(operation_type:, expected_capture_date:) + { "operationType" => operation_type, "transactionDetails" => { "paymentMethodDetails" => { "expectedCaptureDate" => expected_capture_date } } } + end + + test "#find_transaction_by_payment_schedule_item" do + ps = payment_schedules(:payment_schedule_12) + + payment_schedule_item = ps.ordered_items.first + + expected_capture_date = payment_schedule_item.due_date.iso8601 + transactions = [format_transaction(operation_type: "DEBIT", expected_capture_date: expected_capture_date)] + + assert @service.find_transaction_by_payment_schedule_item(transactions, payment_schedule_item) + + expected_capture_date = (payment_schedule_item.due_date - 1.day).iso8601 + transactions = [format_transaction(operation_type: "DEBIT", expected_capture_date: expected_capture_date)] + + assert @service.find_transaction_by_payment_schedule_item(transactions, payment_schedule_item) + + expected_capture_date = (payment_schedule_item.due_date + 1.day).iso8601 + transactions = [format_transaction(operation_type: "DEBIT", expected_capture_date: expected_capture_date)] + + assert @service.find_transaction_by_payment_schedule_item(transactions, payment_schedule_item) + + expected_capture_date = (payment_schedule_item.due_date + 2.days).iso8601 + transactions = [format_transaction(operation_type: "DEBIT", expected_capture_date: expected_capture_date)] + + assert_nil @service.find_transaction_by_payment_schedule_item(transactions, payment_schedule_item) + + expected_capture_date = (payment_schedule_item.due_date - 2.days).iso8601 + transactions = [format_transaction(operation_type: "DEBIT", expected_capture_date: expected_capture_date)] + + assert_nil @service.find_transaction_by_payment_schedule_item(transactions, payment_schedule_item) + + expected_capture_date = payment_schedule_item.due_date.iso8601 + transactions = [format_transaction(operation_type: "CREDIT", expected_capture_date: expected_capture_date)] + + assert_nil @service.find_transaction_by_payment_schedule_item(transactions, payment_schedule_item) + end +end \ No newline at end of file