Skip to content

fix(iyzico): an invoice billed in lira is charged the lira on the invoice - #181

Merged
Panelica merged 4 commits into
Panelica:mainfrom
ertugrulkeyvan:fix/iyzico-invoice-lira-rate
Oct 12, 2026
Merged

Panelica merged 4 commits into
Panelica:mainfrom
ertugrulkeyvan:fix/iyzico-invoice-lira-rate

Conversation

@ertugrulkeyvan

@ertugrulkeyvan ertugrulkeyvan commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

What

iyzico now charges the lira amount the invoice shows, not today's conversion of it.

An iyzico merchant account settles in lira, so the module converts the amount due into TRY before charging. It converted with the TRY rate stored today.

An invoice billed in lira (Setup → Currencies, the invoice's billing stamp) already says what it comes to: its rate was frozen the day it was raised. Take a €10.00 invoice raised at 40: it shows ₺400.00. Once the stored rate had moved to 42, iyzico charged ₺420.00, and the customer saw a different figure on the payment page and on their card than on the invoice.

We bill Turkish customers in lira and take cards through iyzico, so every renewal across a rate change showed the gap.

After this, for an invoice billed in lira:

  • Payment form: charges the amount at the invoice's own rate.
  • Stored-card charge: does the same. It no longer refuses to charge when no TRY rate is stored, since the invoice carries its own.
  • Refund: converts at the rate the payment was actually taken at: iyzico's paidPrice against the amount recorded for the payment (amount_in). That holds both for payments charged at the invoice's rate and for those charged at the stored rate before this change. A full refund gives back exactly the lira that were taken, and a part stays in proportion. Without a recorded payment, the invoice's rate is used, then the stored one.

Unchanged:

  • Every other invoice converts at the stored rate, as before.
  • A shop that prices in TRY passes amounts through untouched.

Recording a payment needed no change. The callback and the stored-card path already convert back with the rate written down when the charge started, which is now the invoice's rate.

How

  • invoiceLiraRate(): the invoice's exchange_rate when it has a billing conversion into TRY, otherwise null.
  • tryPrice(): takes the invoice and prefers that rate. capture() and chargeStoredMethod() pass the invoice.
  • refund(): finds the transaction, the same way the Stripe module does, and converts with paidPrice / amount_in; it falls back to the invoice's rate, then the stored rate. An amount within a cent of what was taken refunds what was taken.

The work is split into four commits: the fix and its tests, then the refund rate from review and its tests.

Tests

tests/Feature/IyzicoInvoiceLiraRateTest.php, 7 tests:

  • Payment form: a lira-billed invoice is charged ₺400.00 at its rate of 40 while the stored rate is 42. The log records 400 / 10 / 40 for the callback.
  • Other invoices: an invoice not billed in lira still converts at the stored rate (₺420.00).
  • Stored card: charged ₺400.00 as well, even with no TRY rate stored.
  • Refund: a refund in full gives back ₺400.00; a part (€2.50) gives back ₺100.00.
  • Refund of an older payment: a payment taken at the stored rate (₺420 for €10, invoice rate 40) is refunded ₺420.00 in full and ₺105.00 for €2.50.

Five of them fail on main or on the first version of this branch. IyzicoGatewayTest and IyzicoStoredCardTest pass unchanged.

Full suite: 4093 passed. The one failure is the CurrencyAndBackupTest db backup test, which fails on main too in my local setup.

@Panelica Panelica left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, @ertugrulkeyvan. Charging the lira the invoice shows is the right behaviour, and the tests make the case clearly.

One thing needs changing before this can go in, in refund():

A payment taken before this change is refunded short. It was charged at the stored rate, not the invoice's. A lira-billed invoice frozen at 40 and paid when the stored rate was 42 took ₺420. A full refund of €10 after this change converts at 40, sends ₺400, and the customer gets ₺20 less than was taken. On main the same refund sends ₺420. We measured this with a test built on yours (paidPrice 420, stored rate 42, invoice rate 40).

The rate the payment was actually taken at is on the payment itself: paidPrice from iyzico against the shop amount recorded on the transaction (amount_in). Converting with that ratio gives back exactly what was taken for payments from before and after the change, and keeps a part refund proportional.

Could you switch refund() to that, and add a test for a payment taken at the stored rate?

@ertugrulkeyvan

Copy link
Copy Markdown
Contributor Author

Thank you, that is a real case I had missed. Pushed:

  • refund(): now converts with the rate the payment was actually taken at, iyzico's paidPrice against the transaction's amount_in, as you suggested. It falls back to the invoice's rate, then the stored rate, only when there is no recorded payment.
  • Test: a payment taken at the stored rate (₺420 for €10, invoice rate 40) is refunded ₺420.00 in full and ₺105.00 for €2.50. Both fail on the previous version of the branch.

The earlier tests still pass, since for payments taken at the invoice's rate the ratio is that rate.

@Panelica
Panelica merged commit 0f9a5b8 into Panelica:main Oct 12, 2026
1 check passed
@Panelica

Copy link
Copy Markdown
Owner

Thank you, @ertugrulkeyvan, merged, with one commit on top. An invoice billed in lira is now charged the lira it shows, from the pay page and from a stored card, and a refund converts at the rate the payment was actually taken at.

The commit we added, 2f91fa2: iyzico answers a refund with the payment's own id, so a refund is recorded under the same transaction id with nothing in amount_in. After a first part refund, the lookup found that row, and a second part refund of a payment taken at the stored rate went back at the invoice's rate (100.00 TRY instead of 105.00). It now reads the payment's own row, and a test covers that case. The full suite passes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants