High precision currency in drupal 7 commerce

In Drupal Commerce you can run into rounding errors when only using 2 significant digits, the easiest solution is to inform commerce to work with 4 significant digits

First instruct Drupal Commerce to use 4 decimals.

1
2
3
4
5
6
7
/**
* Implements hook_commerce_currency_info_alter().
*/
function MY_MODULE_commerce_currency_info_alter(&$info) {
$info['EUR']['decimals'] = 4;
$info['EUR']['format_callback'] = 'MY_MODULE_format_currency_euro';
}

Implement a custom formatter, so the output uses 2 decimals.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* Format EURO currency.
*
* @see commerce_currency_format()
*/
function MY_MODULE_format_currency_euro($amount, $currency, $object) {
$currency['decimals'] = 2;

// Format the price as a number.
$price = number_format(commerce_currency_round(abs($amount), $currency), $currency['decimals'], $currency['decimal_separator'], $currency['thousands_separator']);

// Establish the replacement values to format this price for its currency.
$replacements = array(
'@code_before' => $currency['code_placement'] == 'before' ? $currency['code'] : '',
'@symbol_before' => $currency['symbol_placement'] == 'before' ? $currency['symbol'] : '',
'@price' => $price,
'@symbol_after' => $currency['symbol_placement'] == 'after' ? $currency['symbol'] : '',
'@code_after' => $currency['code_placement'] == 'after' ? $currency['code'] : '',
'@negative' => $amount < 0 ? '-' : '',
'@symbol_spacer' => $currency['symbol_spacer'],
'@code_spacer' => $currency['code_spacer'],
);

return trim(t('@code_before@code_spacer@negative@symbol_before@price@symbol_spacer@symbol_after@code_spacer@code_after', $replacements));
}

Implement the hooks to alter the amount before sending it to the payment provider.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* Implements hook_commerce_ogone_data_alter().
*/
function MY_MODULE_commerce_ogone_data_alter(&$data, $order, $settings) {
$wrapper = entity_metadata_wrapper('commerce_order', $order);
$currency_code = $wrapper->commerce_order_total->currency_code->value();

// Only update EURO.
if ($currency_code !== 'EUR') {
return;
}

$amount = $wrapper->commerce_order_total->amount->value();
$amount = commerce_currency_amount_to_decimal($amount, $currency_code);

$currency = commerce_currency_load($currency_code);
$currency['decimals'] = 2;
$amount = commerce_currency_round($amount, $currency);
$amount = $amount * 100;

$data['AMOUNT'] = $amount;
}

/**
* Implements hook_commerce_stripe_order_charge_alter().
*/
function MY_MODULE_commerce_stripe_order_charge_alter(&$charge, $order) {
$wrapper = entity_metadata_wrapper('commerce_order', $order);
$currency_code = $wrapper->commerce_order_total->currency_code->value();

// Only update EURO.
if ($currency_code !== 'EUR') {
return;
}

$amount = $wrapper->commerce_order_total->amount->value();
$amount = commerce_currency_amount_to_decimal($amount, $currency_code);

$currency = commerce_currency_load($currency_code);
$currency['decimals'] = 2;
$amount = commerce_currency_round($amount, $currency);
$amount = $amount * 100;

$charge['amount'] = $amount;
}

If you want to do this on an existing site, you need to update all amounts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
/**
* Update prices to use 4 decimals.
*/
function MY_MODULE_update_7006() {
// Only update EURO.
if (variable_get('commerce_default_currency', '') !== 'EUR') {
return;
}

db_query('update {field_data_commerce_price} set commerce_price_amount = 100 * commerce_price_amount');
db_query('update {field_data_commerce_total} set commerce_total_amount = 100 * commerce_total_amount');
db_query('update {field_data_commerce_unit_price} set commerce_unit_price_amount = 100 * commerce_unit_price_amount');
db_query('update {field_data_field_commerce_saleprice} set field_commerce_saleprice_amount = 100 * field_commerce_saleprice_amount');
db_query('update {field_data_commerce_order_total} set commerce_order_total_amount = 100 * commerce_order_total_amount');

db_query('update {field_revision_commerce_price} set commerce_price_amount = 100 * commerce_price_amount');
db_query('update {field_revision_commerce_total} set commerce_total_amount = 100 * commerce_total_amount');
db_query('update {field_revision_commerce_unit_price} set commerce_unit_price_amount = 100 * commerce_unit_price_amount');
db_query('update {field_revision_field_commerce_saleprice} set field_commerce_saleprice_amount = 100 * field_commerce_saleprice_amount');
db_query('update {field_revision_commerce_order_total} set commerce_order_total_amount = 100 * commerce_order_total_amount');

db_query('update {commerce_payment_transaction} set amount = 100 * amount');
db_query('update {commerce_payment_transaction_revision} set amount = 100 * amount');

db_query('update {commerce_flat_rate_service} set amount = 100 * amount');

db_query('update {field_data_commerce_fixed_amount} set commerce_fixed_amount_amount = 100 * commerce_fixed_amount_amount');
db_query('update {field_revision_commerce_fixed_amount} set commerce_fixed_amount_amount = 100 * commerce_fixed_amount_amount');
}

/**
* Update prices of completed orders.
*/
function MY_MODULE_update_7007() {
// Only update EURO.
if (variable_get('commerce_default_currency', '') !== 'EUR') {
return;
}

$result = db_query('SELECT * FROM {field_data_commerce_total}');
foreach ($result as $record) {
$data = unserialize($record->commerce_total_data);
if (isset($data['components'])) {
$needs_update = FALSE;
foreach ($data['components'] as &$component) {
if (isset($component['price']['amount'])) {
$component['price']['amount'] = 100 * $component['price']['amount'];
$needs_update = TRUE;
}
}
if ($needs_update) {
$record->commerce_total_data = $data;
drupal_write_record('field_data_commerce_total', $record, array(
'entity_type',
'entity_id',
'delta',
));
}
}
}

$result = db_query('SELECT * FROM {field_revision_commerce_total}');
foreach ($result as $record) {
$data = unserialize($record->commerce_total_data);
if (isset($data['components'])) {
$needs_update = FALSE;
foreach ($data['components'] as &$component) {
if (isset($component['price']['amount'])) {
$component['price']['amount'] = 100 * $component['price']['amount'];
$needs_update = TRUE;
}
}
if ($needs_update) {
$record->commerce_total_data = $data;
drupal_write_record('field_revision_commerce_total', $record, array(
'entity_type',
'entity_id',
'revision_id',
'delta',
));
}
}
}

$result = db_query('SELECT * FROM {field_data_commerce_order_total}');
foreach ($result as $record) {
$data = unserialize($record->commerce_order_total_data);
if (isset($data['components'])) {
$needs_update = FALSE;
foreach ($data['components'] as &$component) {
if (isset($component['price']['amount'])) {
$component['price']['amount'] = 100 * $component['price']['amount'];
$needs_update = TRUE;
}
}
if ($needs_update) {
$record->commerce_order_total_data = $data;
drupal_write_record('field_data_commerce_order_total', $record, array(
'entity_type',
'entity_id',
'delta',
));
}
}
}

$result = db_query('SELECT * FROM {field_revision_commerce_order_total}');
foreach ($result as $record) {
$data = unserialize($record->commerce_order_total_data);
if (isset($data['components'])) {
$needs_update = FALSE;
foreach ($data['components'] as &$component) {
if (isset($component['price']['amount'])) {
$component['price']['amount'] = 100 * $component['price']['amount'];
$needs_update = TRUE;
}
}
if ($needs_update) {
$record->commerce_order_total_data = $data;
drupal_write_record('field_revision_commerce_order_total', $record, array(
'entity_type',
'entity_id',
'revision_id',
'delta',
));
}
}
}
}