/// Hand-written client for `/api/v1/tax`. /// /// **Temporary.** The tax routes are not in `openapi/openapi.json` yet, so /// `app/packages/api_client` has no generated models or methods for them. Everything here /// follows `docs/ai/phase4-contract.md` §5 literally and is meant to be **replaced by the /// generated client** once the routes land in the spec and `just gen-client` runs. /// /// Raw Dio comes from `ref.read(apiProvider).dio`: base URL, bearer header and the one-shot /// refresh on 401 are already wired there. library; import 'package:dio/dio.dart'; import '../../../core/utils/json.dart'; /// Per-account (or total) tax figures for a year. Every number is an **estimate**: the tax /// agent is the broker, and these exist so that the broker's statement can be checked. class TaxRow { const TaxRow({ this.accountId, this.accountName, this.dividendsGrossRub, this.couponsGrossRub, this.taxWithheldRub, this.realizedGainRub, this.realizedLossRub, this.ldvExemptRub, this.taxableBaseRub, this.estimatedTaxRub, }); final int? accountId; final String? accountName; final String? dividendsGrossRub; final String? couponsGrossRub; final String? taxWithheldRub; final String? realizedGainRub; final String? realizedLossRub; final String? ldvExemptRub; final String? taxableBaseRub; final String? estimatedTaxRub; String get title => accountName ?? (accountId == null ? 'Итого' : 'Счёт #$accountId'); static TaxRow fromJson(Map json) => TaxRow( accountId: asInt(json['account_id']), accountName: asString(json['account_name']), dividendsGrossRub: asString(json['dividends_gross_rub']), couponsGrossRub: asString(json['coupons_gross_rub']), taxWithheldRub: asString(json['tax_withheld_rub']), realizedGainRub: asString(json['realized_gain_rub']), realizedLossRub: asString(json['realized_loss_rub']), ldvExemptRub: asString(json['ldv_exempt_rub']), taxableBaseRub: asString(json['taxable_base_rub']), estimatedTaxRub: asString(json['estimated_tax_rub']), ); } class TaxSummary { const TaxSummary({ required this.year, required this.estimated, this.taxRate, this.accounts = const [], this.totals, this.disclaimer, }); final int year; /// Always true per the contract — and shown on screen, not hidden in a tooltip. final bool estimated; final String? taxRate; final List accounts; final TaxRow? totals; final String? disclaimer; static const defaultDisclaimer = 'Оценка. Налоговый агент — брокер; сверяйтесь с его справкой.'; static TaxSummary fromJson(Map json) { final totals = asObject(json['totals']); return TaxSummary( year: asInt(json['year']) ?? DateTime.now().year, estimated: json.containsKey('estimated') ? asBool(json['estimated']) : true, taxRate: asString(json['tax_rate']), accounts: asObjects(json['accounts']).map(TaxRow.fromJson).toList(), totals: totals == null ? null : TaxRow.fromJson(totals), disclaimer: asString(json['disclaimer']), ); } } /// An open lot with the date after which a sale falls under ЛДВ (the three-year exemption). class TaxLot { const TaxLot({ required this.lotId, required this.ldvEligible, this.instrumentId, this.ticker, this.accountId, this.openDate, this.qtyRemaining, this.costRub, this.marketValueRub, this.unrealizedGainRub, this.ldvDate, this.daysToLdv, this.taxIfSoldNowRub, }); final int lotId; final int? instrumentId; final String? ticker; final int? accountId; final DateTime? openDate; final String? qtyRemaining; final String? costRub; final String? marketValueRub; final String? unrealizedGainRub; final bool ldvEligible; final DateTime? ldvDate; final int? daysToLdv; final String? taxIfSoldNowRub; /// Close enough to ЛДВ that selling now is an expensive mistake. Six months is the /// horizon at which a person can still decide to wait. bool get nearLdv => !ldvEligible && daysToLdv != null && daysToLdv! <= 183; String get title => ticker ?? (instrumentId == null ? '#$lotId' : '#$instrumentId'); static TaxLot fromJson(Map json) => TaxLot( lotId: asInt(json['lot_id']) ?? 0, instrumentId: asInt(json['instrument_id']), ticker: asString(json['ticker']), accountId: asInt(json['account_id']), openDate: asDate(json['open_date']), qtyRemaining: asString(json['qty_remaining']), costRub: asString(json['cost_rub']), marketValueRub: asString(json['market_value_rub']), unrealizedGainRub: asString(json['unrealized_gain_rub']), ldvEligible: asBool(json['ldv_eligible']), ldvDate: asDate(json['ldv_date']), daysToLdv: asInt(json['days_to_ldv']), taxIfSoldNowRub: asString(json['tax_if_sold_now_rub']), ); } class TaxApi { const TaxApi(this._dio); final Dio _dio; static const _base = '/api/v1/tax'; Future summary({required int year, int? accountId}) async { final r = await _dio.get>( _base, queryParameters: {'year': year, 'account_id': ?accountId}, ); return TaxSummary.fromJson(r.data ?? const {}); } Future> lots({required int year, int? accountId}) async { final r = await _dio.get>( '$_base/lots', queryParameters: {'year': year, 'account_id': ?accountId}, ); return asObjects((r.data ?? const {})['lots']).map(TaxLot.fromJson).toList(); } }