diff --git a/AGENTS.md b/AGENTS.md index c1ed5ea..002a572 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,6 +8,24 @@ - For cross-module work, apply every relevant scoped instruction file and completion workflow. +## Data and state architecture + +Preserve explicit **DTO → mapper → service → data model → provider → UI** +boundaries across the project. This is a standing requirement for application +features, shared packages, stub services and future mobile clients. + +- DTOs describe transport data; mappers convert it to/from domain data models. +- Services own external I/O and use mappers. Service contracts return data models, + never transport DTOs or untyped response maps to the UI. +- Data models remain distinct from provider state and transport serialization. +- Providers sit on top of services, coordinating loading, errors and state and + exposing models to the UI. Widgets render models and invoke provider actions. +- Keep reusable domain models, services and providers in shared packages; clients + compose them rather than copy business logic or create alternate data paths. +- Preserve these boundaries when replacing stubs with real backend adapters. + Review every changed feature against them before delivery. Apply the detailed + frontend requirements in `frontend/AGENTS.md` to all Flutter clients/packages. + ## UX For any user-facing interface work, follow the diff --git a/README.md b/README.md index da77542..232dcb1 100644 --- a/README.md +++ b/README.md @@ -160,6 +160,16 @@ Before backend integration, agree on the customer, KYC, eligibility/limits and customer-scoped payment contracts for one complete transaction flow. These are intended boundaries, not claims that the required operations already exist. +## Client architecture + +Keep **DTO → mapper → service → data model → provider → UI** boundaries in every +feature and shared package. Services return mapped domain models; providers sit +on services and expose loading/error/data state. Widgets render models and call +provider actions. DTO serialization and backend calls belong below this boundary. +Shared packages own reusable domain logic for web and mobile; stub replacements +must preserve the same contracts. See the standing rules in `AGENTS.md` and +`frontend/AGENTS.md`. + ## Contribution workflow Follow [`AGENTS.md`](AGENTS.md) and the applicable scoped guidelines, including diff --git a/docs/implementation.md b/docs/implementation.md index c7a539c..9eab2b1 100644 --- a/docs/implementation.md +++ b/docs/implementation.md @@ -256,3 +256,26 @@ Validation: all 115 frontend tests, app analysis, app Dart formatting, web build and diff checks pass. Updated existing copy expectations; telemetry tests still verify simulated events never count as real business success. No new click events were needed for this presentation-only cleanup. + +## Data model and provider boundaries — task #30 + +- `architecture-30`, based on workspace PR #29. Root and frontend agent rules, + plus README, now explicitly preserve DTO → mapper → service → data model → + provider → UI across app features, shared packages, stubs and future clients. + Models cannot be replaced by providers or reused as transport DTOs merely + because their fields currently match. +- Checked application service access: widgets compose providers with service + dependencies; they do not parse DTOs or invoke backend operations directly. + KYC and wallet transaction services already map transport data to models. + Auth success responses now pass through explicit response DTOs and mappers; + wallet profile conversion now accepts a ProfileDTO instead of a raw string. +- Existing models, provider state, asynchronous/error behavior and public service + contracts are preserved. DTOs/mappers remain service implementation details. + No UI, telemetry semantics or backend integration changed. +- Frontend review and validation: formatting of all 6 changed Dart files passed; + analysis of sxchnge_auth and sxchnge_wallet passed; all 115 frontend tests + passed; diff check passed. Existing service, provider and integrated tests + exercise the mapping paths. No additional behavioral test was needed for these + direct field projections. No security scan requested. +- Stack order: KYC #21 → limits #23 → workspace #29 → this contribution; + retarget each dependent PR to main after its base is merged. diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md index 78530b8..6111d07 100644 --- a/frontend/AGENTS.md +++ b/frontend/AGENTS.md @@ -12,6 +12,13 @@ skill to perform this completion workflow. ## Preserve the frontend architecture +This applies to `frontend/app`, every shared package and future web/mobile +clients, including in-memory stubs. Keep an explicit data model even when it +currently resembles the DTO; do not use a provider as a substitute for that model. +The UI depends on providers/models; providers depend on service contracts; service +implementations own DTO conversion through mappers. Replacing a service must not +require DTO or networking changes in widgets or providers. + - Strictly preserve the existing `dto` -> `mapper` -> `service` -> `model` -> `provider` flow. Widgets consume models and provider state; they must not bypass the flow by parsing transport data or calling backend APIs directly. diff --git a/frontend/packages/sxchnge_auth/lib/src/dto/results.dart b/frontend/packages/sxchnge_auth/lib/src/dto/results.dart new file mode 100644 index 0000000..79b6dd0 --- /dev/null +++ b/frontend/packages/sxchnge_auth/lib/src/dto/results.dart @@ -0,0 +1,11 @@ +/// Stub response contracts, replaced by generated transport types at integration. +class AuthDTO { + const AuthDTO({required this.userId, required this.simulated}); + final String userId; + final bool simulated; +} + +class VerificationDTO { + const VerificationDTO({required this.simulated}); + final bool simulated; +} diff --git a/frontend/packages/sxchnge_auth/lib/src/mappers/results.dart b/frontend/packages/sxchnge_auth/lib/src/mappers/results.dart new file mode 100644 index 0000000..b1456b1 --- /dev/null +++ b/frontend/packages/sxchnge_auth/lib/src/mappers/results.dart @@ -0,0 +1,13 @@ +import '../dto/results.dart'; +import '../models/auth.dart'; +import '../models/verification.dart'; + +abstract final class AuthMapper { + static AuthResult fromDTO(AuthDTO dto) => + AuthResult(userId: dto.userId, isSimulated: dto.simulated); +} + +abstract final class VerificationMapper { + static VerificationResult fromDTO(VerificationDTO dto) => + VerificationResult(isSimulated: dto.simulated); +} diff --git a/frontend/packages/sxchnge_auth/lib/src/services/dummy.dart b/frontend/packages/sxchnge_auth/lib/src/services/dummy.dart index 2076781..28ba934 100644 --- a/frontend/packages/sxchnge_auth/lib/src/services/dummy.dart +++ b/frontend/packages/sxchnge_auth/lib/src/services/dummy.dart @@ -1,3 +1,5 @@ +import '../dto/results.dart'; +import '../mappers/results.dart'; import '../models/auth.dart'; import 'contracts.dart'; import 'verification.dart'; @@ -18,7 +20,9 @@ class DummyLoginService implements LoginService, SimulationInfo { Future login(LoginRequest request) async { await Future.delayed(delay); if (failure case final error?) throw AuthException(error); - return const AuthResult(userId: 'demo-user', isSimulated: true); + return AuthMapper.fromDTO( + const AuthDTO(userId: 'demo-user', simulated: true), + ); } } @@ -36,6 +40,8 @@ class DummySignupService implements SignupService, SimulationInfo { Future signup(SignupRequest request) async { await Future.delayed(delay); if (failure case final error?) throw AuthException(error); - return const AuthResult(userId: 'demo-user', isSimulated: true); + return AuthMapper.fromDTO( + const AuthDTO(userId: 'demo-user', simulated: true), + ); } } diff --git a/frontend/packages/sxchnge_auth/lib/src/services/verification.dart b/frontend/packages/sxchnge_auth/lib/src/services/verification.dart index 275e0e7..9ce505e 100644 --- a/frontend/packages/sxchnge_auth/lib/src/services/verification.dart +++ b/frontend/packages/sxchnge_auth/lib/src/services/verification.dart @@ -1,3 +1,5 @@ +import '../dto/results.dart'; +import '../mappers/results.dart'; import '../models/verification.dart'; abstract interface class SimulationInfo { @@ -30,7 +32,7 @@ class DummyVerificationService implements VerificationService, SimulationInfo { }) async { await Future.delayed(delay); if (sendFailure case final error?) throw VerificationException(error); - return const VerificationResult(isSimulated: true); + return VerificationMapper.fromDTO(const VerificationDTO(simulated: true)); } @override @@ -38,7 +40,9 @@ class DummyVerificationService implements VerificationService, SimulationInfo { await Future.delayed(delay); switch (token) { case 'demo-valid': - return const VerificationResult(isSimulated: true); + return VerificationMapper.fromDTO( + const VerificationDTO(simulated: true), + ); case 'demo-expired': throw const VerificationException(VerificationError.expired); case 'demo-internal': diff --git a/frontend/packages/sxchnge_wallet/lib/src/services/dummy.dart b/frontend/packages/sxchnge_wallet/lib/src/services/dummy.dart index ec8cde2..8488fb5 100644 --- a/frontend/packages/sxchnge_wallet/lib/src/services/dummy.dart +++ b/frontend/packages/sxchnge_wallet/lib/src/services/dummy.dart @@ -204,7 +204,7 @@ class DummyWalletService implements WalletService { @override Future profile() async { await _wait(); - return WalletMapper.profile(_locale); + return WalletMapper.profile(ProfileDTO(_locale)); } @override @@ -213,7 +213,7 @@ class DummyWalletService implements WalletService { if (!['en', 'ru'].contains(locale)) throw const WalletException(WalletError.unsupported); _locale = locale; - return WalletMapper.profile(_locale); + return WalletMapper.profile(ProfileDTO(_locale)); } // Kept separate from wallet fetch because LimitsService has a different result. diff --git a/frontend/packages/sxchnge_wallet/lib/src/transport/wallet.dart b/frontend/packages/sxchnge_wallet/lib/src/transport/wallet.dart index 2614608..445b496 100644 --- a/frontend/packages/sxchnge_wallet/lib/src/transport/wallet.dart +++ b/frontend/packages/sxchnge_wallet/lib/src/transport/wallet.dart @@ -53,6 +53,11 @@ class OperationDTO { final bool fail; } +class ProfileDTO { + const ProfileDTO(this.locale); + final String locale; +} + abstract final class WalletMapper { static CurrencyAccount account(AccountDTO d) => CurrencyAccount( id: d.id, @@ -85,5 +90,5 @@ abstract final class WalletMapper { status: OperationStatus.values.byName(d.status), createdAt: d.created, ); - static AccountProfile profile(String locale) => AccountProfile(locale); + static AccountProfile profile(ProfileDTO dto) => AccountProfile(dto.locale); }