Preserve explicit data model and provider boundaries #31
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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);
|
||||
}
|
||||
Reference in new issue
Block a user