%s %s
',
esc_html__('Calculated by', 'flatin-pt'),
esc_url(FLATIN_PT_SITE),
esc_html('flatin.pt')
);
}
private static function notice($message)
{
return sprintf(
'%s
',
esc_html($message)
);
}
/**
* Validation error, shown as the plugin's own translatable string.
*
* The API's free-text `message` field is never printed: it is not part of
* the plugin's translation catalogue, so only the error code is mapped.
*/
private static function refusal($data, $asked)
{
$code = isset($data['error']) ? (string) $data['error'] : '';
switch ($code) {
case 'not_found':
$text = sprintf(
/* translators: %s: municipality as written in the shortcode. */
__('No municipality "%s" in the table. Use its name or its four-digit code.', 'flatin-pt'),
$asked
);
break;
case 'empty_value':
case 'bad_value':
$text = __('The price is not an amount. Example: [flatin_imt value="250000"]', 'flatin-pt');
break;
case 'not_positive':
$text = __('The price has to be greater than zero.', 'flatin-pt');
break;
case 'too_large':
$text = isset($data['field']) && 'vpt' === $data['field']
? __('That VPT is very high: check for one zero too many.', 'flatin-pt')
: __('That price is very high: check for one zero too many.', 'flatin-pt');
break;
case 'bad_vpt':
$text = __('The VPT is not an amount. Example: [flatin_imt value="250000" vpt="180000"]', 'flatin-pt');
break;
case 'not_positive_vpt':
$text = __('The VPT has to be greater than zero.', 'flatin-pt');
break;
default:
// Unknown code: fall back to the generic message and link.
return self::unavailable(null);
}
return '' . self::notice($text) . '
';
}
/**
* API failure: never break the host page.
*
* Instead of an empty block or a fatal error, show a short notice, plus a
* link to the calculator on flatin.pt if the site owner allowed links.
*/
private static function unavailable($error)
{
// Only the plugin's own `flatin_api` message is shown. Transport errors
// carry raw WordPress/cURL text that is not meant for site visitors.
$text = is_wp_error($error) && 'flatin_api' === $error->get_error_code()
? $error->get_error_message()
: __('The calculation service is unavailable.', 'flatin-pt');
$html = '' . self::notice($text);
if (self::links_allowed()) {
$html .= sprintf(
'
%s
',
esc_url(FLATIN_PT_SITE . 'en/tools/imt-calculator/'),
esc_html__('Open the calculator on flatin.pt', 'flatin-pt')
);
}
return $html . '
';
}
/**
* [flatin_imt value="250000" territory="continente" primary="yes" young="no" vpt="180000"]
*/
public static function imt($atts)
{
self::$used = true;
wp_enqueue_style('flatin-pt');
$a = shortcode_atts([
'value' => '',
'territory' => 'continente',
'primary' => 'yes',
'young' => 'no',
'vpt' => '',
], $atts, 'flatin_imt');
if ('' === trim((string) $a['value'])) {
return ''
. self::notice(__('Add a price: [flatin_imt value="250000"]', 'flatin-pt'))
. '
';
}
$data = Flatin_Client::imt([
'value' => $a['value'],
'territory' => 'regioes_autonomas' === $a['territory']
? 'regioes_autonomas'
: 'continente',
'primary_home' => self::flag($a['primary'], true),
'buyer_is_young' => self::flag($a['young'], false),
'vpt' => trim((string) $a['vpt']),
]);
if (is_wp_error($data)) {
return self::unavailable($data);
}
if (isset($data['error'])) {
return self::refusal($data, $a['value']);
}
$f = isset($data['formatted']) ? $data['formatted'] : [];
$rows = [
__('IMT', 'flatin-pt') => isset($f['imt']) ? $f['imt'] : '',
__('Stamp duty', 'flatin-pt') => isset($f['stamp_duty']) ? $f['stamp_duty'] : '',
// Not "the deed", and not an estimate: the API returns the
// published price of one registration act. The label is the one
// the calculator on flatin.pt prints, so a reader who follows the
// link finds the same line under the same name.
__('Property registration (Casa Pronta)', 'flatin-pt') => isset($f['notary_estimate']) ? $f['notary_estimate'] : '',
];
$html = '';
$html .= '
';
foreach ($rows as $label => $value) {
$html .= sprintf(
'| %s | %s |
',
esc_html($label),
esc_html($value)
);
}
$html .= sprintf(
'| %s | %s |
',
esc_html__('Total on top of the price', 'flatin-pt'),
esc_html(isset($f['total']) ? $f['total'] : '')
);
$html .= '
';
// Which value the taxes were calculated on. IMT and stamp duty are
// charged on the higher of the contract price and the VPT (CIMT
// art. 12), and a reader who sees only a price assumes it is the base.
$html .= sprintf('
%s
', esc_html(self::base_note($data)));
// Where the figure comes from, in words: the plugin writes into
// someone else's page, where a link would need the credit opt-in
// (directory rule 10), and the source is checkable without one. The
// amounts come with the answer, from the same price list the figure
// was taken from, so a new tariff reaches this note without a release.
$tariff = isset($data['deed_source']['formatted']) ? $data['deed_source']['formatted'] : [];
if (isset($tariff['single_act'], $tariff['extra_property'])) {
$registration_note = sprintf(
/* translators: 1: price of one registration act, 2: price per additional property, both already formatted in euros. */
__('The published price of the Casa Pronta counter of the Ministério da Justiça: %1$s for one registration act, plus %2$s for each additional property. That is the price of the registration. The purchase contract itself, if it is drawn up by a notary or a solicitador, is charged separately and has no published tariff.', 'flatin-pt'),
$tariff['single_act'],
$tariff['extra_property']
);
} else {
$registration_note = __('The published price of the Casa Pronta counter of the Ministério da Justiça. That is the price of the registration. The purchase contract itself, if it is drawn up by a notary or a solicitador, is charged separately and has no published tariff.', 'flatin-pt');
}
$html .= sprintf('
%s
', esc_html($registration_note));
// The calculator on flatin.pt says this in its own "what this total
// does not include" block. The shortcode has no such block, and a
// buyer with a mortgage would otherwise read this total as theirs.
$html .= sprintf(
'
%s
',
esc_html__(
'Buying with a mortgage costs more than this total: there is a second registration act, and there is stamp duty on the loan itself (item 17.1).',
'flatin-pt'
)
);
$html .= self::attribution();
$html .= '
';
return $html;
}
/**
* One line on the base of the taxes: the price, or the VPT when higher.
*/
private static function base_note($data)
{
$f = isset($data['formatted']) ? $data['formatted'] : [];
if (isset($data['taxable_base']) && 'vpt' === $data['taxable_base']) {
return sprintf(
/* translators: %s: the VPT, already formatted in euros. */
__('IMT and stamp duty are calculated on the VPT, %s, because it is higher than the price.', 'flatin-pt'),
isset($f['taxable_value']) ? $f['taxable_value'] : ''
);
}
if (isset($data['input']['vpt_cents']) && null !== $data['input']['vpt_cents']) {
return __('IMT and stamp duty are calculated on the price: the VPT is not higher.', 'flatin-pt');
}
return __('Calculated on the price. If the VPT of the property is higher, IMT and stamp duty are charged on the VPT and come out higher.', 'flatin-pt');
}
/**
* [flatin_imi concelho="porto"]
*/
public static function imi($atts)
{
self::$used = true;
wp_enqueue_style('flatin-pt');
$a = shortcode_atts(['concelho' => '', 'vpt' => ''], $atts, 'flatin_imi');
if ('' === trim((string) $a['concelho'])) {
return ''
. self::notice(__('Add a municipality: [flatin_imi concelho="porto"]', 'flatin-pt'))
. '
';
}
$data = Flatin_Client::imi_rate($a['concelho']);
if (is_wp_error($data)) {
return self::unavailable($data);
}
if (isset($data['error'])) {
return self::refusal($data, trim((string) $a['concelho']));
}
$name = isset($data['municipality']) ? $data['municipality'] : $a['concelho'];
$urban = isset($data['urban']) ? $data['urban'] : [];
$html = '';
if (empty($urban['available'])) {
// No single municipal rate: say so rather than printing a zero.
$html .= sprintf(
'
%s
',
esc_html(sprintf(
/* translators: %s: municipality name. */
__('%s sets no single IMI rate: it is set per parish (freguesia).', 'flatin-pt'),
$name
))
);
} else {
$html .= sprintf(
'
%s %s
',
esc_html(sprintf(
/* translators: 1: municipality name, 2: tax year. */
__('IMI rate in %1$s, %2$s:', 'flatin-pt'),
$name,
isset($data['year']) ? $data['year'] : ''
)),
esc_html($urban['percent'])
);
$vpt = self::money($a['vpt']);
if (null !== $vpt && isset($urban['thousandths_of_percent'])) {
$annual = (int) round($vpt * (int) $urban['thousandths_of_percent'] / 100000);
$html .= sprintf(
'
%s
',
esc_html(sprintf(
/* translators: 1: taxable value, 2: yearly tax amount. */
__('At a taxable value (VPT) of %1$s the yearly IMI is about %2$s.', 'flatin-pt'),
self::euros($vpt),
self::euros($annual)
))
);
}
}
$html .= self::attribution();
$html .= '
';
return $html;
}
private static function flag($raw, $fallback)
{
$raw = strtolower(trim((string) $raw));
if (in_array($raw, ['yes', 'true', '1', 'on'], true)) {
return true;
}
if (in_array($raw, ['no', 'false', '0', 'off'], true)) {
return false;
}
return $fallback;
}
/** Euros to cents; `null` if the input is not an amount. */
private static function money($raw)
{
$raw = trim(str_replace([' ', "\xC2\xA0"], '', (string) $raw));
if ('' === $raw) {
return null;
}
$raw = str_replace(',', '.', $raw);
if (!is_numeric($raw)) {
return null;
}
return (int) round((float) $raw * 100);
}
/**
* Format an amount the Portuguese way: comma decimal, space thousands.
*
* The site locale is deliberately not used, so locally computed amounts
* match the amounts formatted by the API in the same block.
*/
private static function euros($cents)
{
return number_format($cents / 100, 2, ',', ' ') . ' €';
}
}