CluPilotCloud/app/Support/Money.php

54 lines
2.0 KiB
PHP

<?php
namespace App\Support;
/**
* Between what an operator types and what the catalogue stores.
*
* Prices are stored in cents — integers, because money in floats eventually
* bills someone 178.99999999. But the console asked the owner to type cents
* too, so a €799 plan was entered as 79900, and a slipped digit is a factor of
* ten on an invoice. The form takes euros; the conversion happens here, once.
*
* Parsing is done on the STRING, not by multiplying a float: (float) '79.90'
* times 100 is 7989.999… on a binary float, and casting that to int truncates
* to 7989. One cent, every time, on exactly the prices people actually charge.
*/
final class Money
{
/** What plan_prices.amount_cents can actually hold (unsigned int). */
public const MAX_CENTS = 4294967295;
/**
* Euros as typed → cents, or null when it is not a price at all.
*
* Accepts both separators: a German keyboard produces "79,90" and a copied
* figure produces "79.90", and rejecting either would just be rude.
* Thousands separators are refused rather than guessed — "1.000" is a
* thousand to one reader and one euro to another.
*/
public static function toCents(string $euros): ?int
{
$value = str_replace(',', '.', trim($euros));
if (! preg_match('/^\d{1,9}(\.\d{1,2})?$/', $value)) {
return null;
}
[$whole, $fraction] = array_pad(explode('.', $value, 2), 2, '');
$cents = (int) $whole * 100 + (int) str_pad($fraction, 2, '0');
// The column is an unsigned int. Nine whole digits parse happily and
// then overflow it, turning a typo into a database error on submit
// instead of a message next to the field.
return $cents > self::MAX_CENTS ? null : $cents;
}
/** Cents → the string the form shows, with a decimal comma. */
public static function fromCents(int $cents): string
{
return number_format($cents / 100, 2, ',', '');
}
}