%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( '', esc_html($label), esc_html($value) ); } $html .= sprintf( '', esc_html__('Total on top of the price', 'flatin-pt'), esc_html(isset($f['total']) ? $f['total'] : '') ); $html .= '
%s%s
%s%s
'; // 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, ',', ' ') . ' €'; } }