<?php

namespace App\Support;

/**
 * How many minor units make one unit of a currency.
 *
 * Every ad platform takes money in the currency's smallest unit and gives it
 * back the same way, and the conversion was a hardcoded 100 in six places. That
 * is right for the dollar and wrong for the yen, which has no subunit at all: a
 * budget of 3000 on a JPY ad account was sent as 300000, a hundred times what
 * the buyer approved and what the ceiling had checked.
 *
 * It hid because the two paths that read the number back divided by 100 as
 * well, so the audit line and a re-import both reported the approved figure
 * while the platform held a hundred times it.
 *
 * ISO 4217 exponents, because that is what "smallest unit of the currency"
 * means and it is the definition the platforms themselves point at. Only the
 * currencies that are not two are listed; everything else is two, which keeps
 * this a strict correction rather than a change to the common case.
 */
final class CurrencyMinorUnits
{
    /**
     * Currencies whose exponent is not 2, by ISO 4217.
     *
     * @var array<string, int>
     */
    private const EXPONENT = [
        // No subunit. A budget here is already an integer number of units.
        'BIF' => 0, 'CLP' => 0, 'DJF' => 0, 'GNF' => 0, 'ISK' => 0,
        'JPY' => 0, 'KMF' => 0, 'KRW' => 0, 'PYG' => 0, 'RWF' => 0,
        'UGX' => 0, 'UYI' => 0, 'VND' => 0, 'VUV' => 0, 'XAF' => 0,
        'XOF' => 0, 'XPF' => 0,

        // Three, so one unit is a thousand minor units.
        'BHD' => 3, 'IQD' => 3, 'JOD' => 3, 'KWD' => 3,
        'LYD' => 3, 'OMR' => 3, 'TND' => 3,
    ];

    /** How many decimal places an amount in this currency may have. */
    public static function decimals(?string $currency): int
    {
        return self::EXPONENT[strtoupper(trim((string) $currency))] ?? 2;
    }

    /** Minor units in one unit: 100 for the dollar, 1 for the yen. */
    public static function factor(?string $currency): int
    {
        return 10 ** self::decimals($currency);
    }

    /**
     * An amount rounded to something the currency can actually express.
     *
     * Dividing money needs this as much as sending it does. A three-way split of
     * 1000 on a JPY account was rounded to 333.33 each, which is not an amount
     * of yen: toMinor() then rounds each share to 333 and the three no longer
     * add up to the total the buyer agreed, which is the one thing the split is
     * supposed to guarantee.
     */
    public static function round(float|string|null $amount, ?string $currency): float
    {
        return round((float) $amount, self::decimals($currency));
    }

    /**
     * An amount of money as the integer the platform wants.
     *
     * Rounded rather than truncated, so 0.005 on a two-decimal currency is a
     * cent rather than nothing.
     */
    public static function toMinor(float|string|null $amount, ?string $currency): int
    {
        return (int) round((float) $amount * self::factor($currency));
    }

    /** And back, for reading a platform's own figure. */
    public static function toMajor(int|string|null $minorUnits, ?string $currency): float
    {
        return ((int) $minorUnits) / self::factor($currency);
    }
}
