Preserve explicit data model and provider boundaries #31

Merged
tech merged 4 commits from architecture-30 into main 2026-09-12 10:14:03 +00:00
10 changed files with 104 additions and 7 deletions
Showing only changes of commit 8b0662e340 - Show all commits

No files matched your search

+18
View File
@@ -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
+10
View File
@@ -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
+23
View File
@@ -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.
+7
View File
@@ -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.
@@ -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;
}
@@ -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);
}
@@ -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<AuthResult> login(LoginRequest request) async {
await Future<void>.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<AuthResult> signup(SignupRequest request) async {
await Future<void>.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),
);
}
}
@@ -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<void>.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<void>.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':
@@ -204,7 +204,7 @@ class DummyWalletService implements WalletService {
@override
Future<AccountProfile> 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.
@@ -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);
}