Preserve explicit model and provider architecture (#30)
This commit is contained in:
1 parent
54f50ad6b7
commit
8b0662e340
10 files changed
+104
-7
No files matched your search
@@ -8,6 +8,24 @@
|
|||||||
- For cross-module work, apply every relevant scoped instruction file and
|
- For cross-module work, apply every relevant scoped instruction file and
|
||||||
completion workflow.
|
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
|
## UX
|
||||||
|
|
||||||
For any user-facing interface work, follow the
|
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
|
customer-scoped payment contracts for one complete transaction flow. These are
|
||||||
intended boundaries, not claims that the required operations already exist.
|
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
|
## Contribution workflow
|
||||||
|
|
||||||
Follow [`AGENTS.md`](AGENTS.md) and the applicable scoped guidelines, including
|
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
|
and diff checks pass. Updated existing copy expectations; telemetry tests still
|
||||||
verify simulated events never count as real business success. No new click events
|
verify simulated events never count as real business success. No new click events
|
||||||
were needed for this presentation-only cleanup.
|
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
|
## 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` ->
|
- Strictly preserve the existing `dto` -> `mapper` -> `service` -> `model` ->
|
||||||
`provider` flow. Widgets consume models and provider state; they must not
|
`provider` flow. Widgets consume models and provider state; they must not
|
||||||
bypass the flow by parsing transport data or calling backend APIs directly.
|
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 '../models/auth.dart';
|
||||||
import 'contracts.dart';
|
import 'contracts.dart';
|
||||||
import 'verification.dart';
|
import 'verification.dart';
|
||||||
@@ -18,7 +20,9 @@ class DummyLoginService implements LoginService, SimulationInfo {
|
|||||||
Future<AuthResult> login(LoginRequest request) async {
|
Future<AuthResult> login(LoginRequest request) async {
|
||||||
await Future<void>.delayed(delay);
|
await Future<void>.delayed(delay);
|
||||||
if (failure case final error?) throw AuthException(error);
|
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 {
|
Future<AuthResult> signup(SignupRequest request) async {
|
||||||
await Future<void>.delayed(delay);
|
await Future<void>.delayed(delay);
|
||||||
if (failure case final error?) throw AuthException(error);
|
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';
|
import '../models/verification.dart';
|
||||||
|
|
||||||
abstract interface class SimulationInfo {
|
abstract interface class SimulationInfo {
|
||||||
@@ -30,7 +32,7 @@ class DummyVerificationService implements VerificationService, SimulationInfo {
|
|||||||
}) async {
|
}) async {
|
||||||
await Future<void>.delayed(delay);
|
await Future<void>.delayed(delay);
|
||||||
if (sendFailure case final error?) throw VerificationException(error);
|
if (sendFailure case final error?) throw VerificationException(error);
|
||||||
return const VerificationResult(isSimulated: true);
|
return VerificationMapper.fromDTO(const VerificationDTO(simulated: true));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -38,7 +40,9 @@ class DummyVerificationService implements VerificationService, SimulationInfo {
|
|||||||
await Future<void>.delayed(delay);
|
await Future<void>.delayed(delay);
|
||||||
switch (token) {
|
switch (token) {
|
||||||
case 'demo-valid':
|
case 'demo-valid':
|
||||||
return const VerificationResult(isSimulated: true);
|
return VerificationMapper.fromDTO(
|
||||||
|
const VerificationDTO(simulated: true),
|
||||||
|
);
|
||||||
case 'demo-expired':
|
case 'demo-expired':
|
||||||
throw const VerificationException(VerificationError.expired);
|
throw const VerificationException(VerificationError.expired);
|
||||||
case 'demo-internal':
|
case 'demo-internal':
|
||||||
|
|||||||
@@ -204,7 +204,7 @@ class DummyWalletService implements WalletService {
|
|||||||
@override
|
@override
|
||||||
Future<AccountProfile> profile() async {
|
Future<AccountProfile> profile() async {
|
||||||
await _wait();
|
await _wait();
|
||||||
return WalletMapper.profile(_locale);
|
return WalletMapper.profile(ProfileDTO(_locale));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -213,7 +213,7 @@ class DummyWalletService implements WalletService {
|
|||||||
if (!['en', 'ru'].contains(locale))
|
if (!['en', 'ru'].contains(locale))
|
||||||
throw const WalletException(WalletError.unsupported);
|
throw const WalletException(WalletError.unsupported);
|
||||||
_locale = locale;
|
_locale = locale;
|
||||||
return WalletMapper.profile(_locale);
|
return WalletMapper.profile(ProfileDTO(_locale));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Kept separate from wallet fetch because LimitsService has a different result.
|
// Kept separate from wallet fetch because LimitsService has a different result.
|
||||||
|
|||||||
@@ -53,6 +53,11 @@ class OperationDTO {
|
|||||||
final bool fail;
|
final bool fail;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ProfileDTO {
|
||||||
|
const ProfileDTO(this.locale);
|
||||||
|
final String locale;
|
||||||
|
}
|
||||||
|
|
||||||
abstract final class WalletMapper {
|
abstract final class WalletMapper {
|
||||||
static CurrencyAccount account(AccountDTO d) => CurrencyAccount(
|
static CurrencyAccount account(AccountDTO d) => CurrencyAccount(
|
||||||
id: d.id,
|
id: d.id,
|
||||||
@@ -85,5 +90,5 @@ abstract final class WalletMapper {
|
|||||||
status: OperationStatus.values.byName(d.status),
|
status: OperationStatus.values.byName(d.status),
|
||||||
createdAt: d.created,
|
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