Skip to content

Send the transaction amount to Paystack as an integer number of kobo - #70

Merged
jules-paystack merged 1 commit into
PaystackHQ:masterfrom
iammcoding:fix-kobo-amount-rounding
Aug 17, 2026
Merged

jules-paystack merged 1 commit into
PaystackHQ:masterfrom
iammcoding:fix-kobo-amount-rounding

Conversation

@iammcoding

Copy link
Copy Markdown
Contributor

The amount was calculated as $order->getGrandTotal() * 100. Grand totals are floating point, so multiplying by 100 introduces the usual float representation error and the value is then JSON encoded and sent to Paystack as a fractional number.

For example a 19.99 order total becomes 1998.9999999999998 instead of 1999, and is sent as {"amount":1998.9999999999998}. Paystack expects the amount in the currency subunit (kobo) as an integer, so a fractional amount is either rejected or truncated, which undercharges the customer. Other common totals hit this too: 1.10 -> 110.00000000000001, 0.29 -> 28.999999999999996, 8.21 -> 821.0000000000001.

Wrapping the calculation in (int) round(...) sends a correct integer amount for every total.

The amount was calculated as $order->getGrandTotal() * 100. Grand totals
are floating point, so multiplying by 100 introduces the usual float
representation error and the value is then JSON encoded and sent to
Paystack as a fractional number.

For example a 19.99 order total becomes 1998.9999999999998 instead of
1999, and is sent as {"amount":1998.9999999999998}. Paystack expects the
amount in the currency subunit (kobo) as an integer, so a fractional
amount is either rejected or truncated, which undercharges the customer.
Other common totals hit this too: 1.10 -> 110.00000000000001,
0.29 -> 28.999999999999996, 8.21 -> 821.0000000000001.

Wrapping the calculation in (int) round(...) sends a correct integer
amount for every total.

@jules-paystack jules-paystack left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Confirmed empirically against the Paystack API: a fractional amount is rejected outright, not truncated —

amount=1998.9999999999998 → {"status":false,"message":"\"amount\" must be an integer","code":"invalid_amount"}
amount=1999                → {"status":true,"message":"Authorization URL created"}

So the impact is stronger than the description suggests: it is not an undercharge, it is a hard checkout failure — PaystackApiClient::request() throws on status:false, Setup::execute() catches it and redirects the customer to the failure page, so affected orders cannot be paid at all on the Standard/redirect flow.

Note PHP's json_encode (serialize_precision=-1) emits integer-valued floats without a decimal point, so only genuinely inexact products break — json_encode([5000.00*100, 19.99*100]) → [500000, 1998.9999999999998]. That intermittency is why this went unnoticed.

Thanks for the fix. Merging.

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