feat(app): экраны импорта отчётов, целей, доходов, ребалансировки, налогов и бенчмарков
Импорт (/imports, /instruments/pending) и фаза 4 (/goals, /income, /rebalance, /tax, аналитика-хаб с benchmarks_card) — по docs/ai/import-contract.md и docs/ai/phase4-contract.md. file_picker для загрузки отчёта.
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
/// 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<String, dynamic> 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<TaxRow> accounts;
|
||||
final TaxRow? totals;
|
||||
final String? disclaimer;
|
||||
|
||||
static const defaultDisclaimer =
|
||||
'Оценка. Налоговый агент — брокер; сверяйтесь с его справкой.';
|
||||
|
||||
static TaxSummary fromJson(Map<String, dynamic> 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<String, dynamic> 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<TaxSummary> summary({required int year, int? accountId}) async {
|
||||
final r = await _dio.get<Map<String, dynamic>>(
|
||||
_base,
|
||||
queryParameters: {'year': year, 'account_id': ?accountId},
|
||||
);
|
||||
return TaxSummary.fromJson(r.data ?? const {});
|
||||
}
|
||||
|
||||
Future<List<TaxLot>> lots({required int year, int? accountId}) async {
|
||||
final r = await _dio.get<Map<String, dynamic>>(
|
||||
'$_base/lots',
|
||||
queryParameters: {'year': year, 'account_id': ?accountId},
|
||||
);
|
||||
return asObjects((r.data ?? const {})['lots']).map(TaxLot.fromJson).toList();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user