feat(backend): Implement Subscription Status Endpoint
Task ID: BACKEND-002 Priority: high Changes: Completed by: AI Agent Duration: 64625ms
This commit is contained in:
parent
311894318d
commit
eabf5f354a
4 changed files with 210 additions and 5 deletions
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"component": "backend",
|
||||
"current_task_id": "BACKEND-001",
|
||||
"iteration_count": 1,
|
||||
"current_task_id": "BACKEND-002",
|
||||
"iteration_count": 2,
|
||||
"max_iterations": 10,
|
||||
"started_at": "2025-11-21T01:06:15.789515+00:00",
|
||||
"last_commit": null,
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@
|
|||
"id": "BACKEND-002",
|
||||
"title": "Implement Subscription Status Endpoint",
|
||||
"priority": "high",
|
||||
"status": "pending",
|
||||
"status": "in_progress",
|
||||
"estimated_hours": 2.0,
|
||||
"description": "Implement GET /api/v2/subscriptions/status endpoint to return current user's subscription status. Use SubscriptionManager.getSubscriptionDto() to get subscription info and return active status, start/finish dates.",
|
||||
"acceptance_criteria": [
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'dart:convert';
|
|||
import 'dart:developer' as developer;
|
||||
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:mnemo_cards_backend/api/authorize/helpers.dart';
|
||||
import 'package:mnemo_cards_backend/api/subscription/subscription_manager.dart';
|
||||
import 'package:shelf/shelf.dart';
|
||||
import 'package:shelf_open_api/shelf_open_api.dart';
|
||||
|
|
@ -40,6 +41,15 @@ class SubscriptionsApiV2 {
|
|||
headers: {'Content-Type': 'application/json'},
|
||||
);
|
||||
|
||||
Response _unauthorized() => Response(
|
||||
401,
|
||||
body: jsonEncode({
|
||||
'error': 'unauthorized',
|
||||
'message': 'Authentication required',
|
||||
}),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
);
|
||||
|
||||
/// GET /api/v2/subscriptions/plans
|
||||
/// List available subscription plans
|
||||
/// Returns all available subscription plans. Authentication is optional.
|
||||
|
|
@ -75,8 +85,27 @@ class SubscriptionsApiV2 {
|
|||
@Route.get('/subscriptions/status')
|
||||
@OpenApiRoute()
|
||||
Future<Response> getStatus(Request request) async {
|
||||
// TODO: implement
|
||||
return _ok({'active': false});
|
||||
try {
|
||||
final user = request.user;
|
||||
if (user == null) {
|
||||
return _unauthorized();
|
||||
}
|
||||
|
||||
final subscriptionDto = await _subscriptionManager.getSubscriptionDto(user);
|
||||
|
||||
return _ok({
|
||||
'active': subscriptionDto.isActive,
|
||||
if (subscriptionDto.start != null) 'start': subscriptionDto.start!.toIso8601String(),
|
||||
if (subscriptionDto.finish != null) 'finish': subscriptionDto.finish!.toIso8601String(),
|
||||
});
|
||||
} catch (e, s) {
|
||||
developer.log(
|
||||
'Error in getStatus: $e',
|
||||
error: e,
|
||||
stackTrace: s,
|
||||
);
|
||||
return _internalServerError(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/// POST /api/v2/subscriptions/cancel
|
||||
|
|
|
|||
|
|
@ -301,4 +301,180 @@ void main() {
|
|||
expect(paymentSystems, contains('google'));
|
||||
});
|
||||
});
|
||||
|
||||
group('SubscriptionsApiV2 - Get Status', () {
|
||||
test('should return 401 for unauthenticated request', () async {
|
||||
final request = Request(
|
||||
'GET',
|
||||
Uri.parse('http://localhost/api/v2/subscriptions/status'),
|
||||
);
|
||||
|
||||
final response = await subscriptionsApiV2.getStatus(request);
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(401));
|
||||
expect(responseBody['error'], equals('unauthorized'));
|
||||
expect(responseBody['message'], equals('Authentication required'));
|
||||
});
|
||||
|
||||
test('should return active: false when user has no subscription', () async {
|
||||
final request = Request(
|
||||
'GET',
|
||||
Uri.parse('http://localhost/api/v2/subscriptions/status'),
|
||||
).change(context: {'user': testUser});
|
||||
|
||||
final response = await subscriptionsApiV2.getStatus(request);
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
expect(responseBody['active'], isFalse);
|
||||
expect(responseBody.containsKey('start'), isFalse);
|
||||
expect(responseBody.containsKey('finish'), isFalse);
|
||||
});
|
||||
|
||||
test('should return active: true with dates when subscription is active', () async {
|
||||
final now = DateTime.now();
|
||||
final startDate = now.subtract(const Duration(days: 5));
|
||||
final finishDate = now.add(const Duration(days: 25));
|
||||
|
||||
// Create active subscription for test user
|
||||
await testIsar.writeTxn(() async {
|
||||
final subscription = UserSubscriptionModel(
|
||||
start: startDate,
|
||||
finish: finishDate,
|
||||
features: [SubscriptionFeatureEnum.packs],
|
||||
);
|
||||
await testIsar.userSubscriptionModels.put(subscription);
|
||||
|
||||
final user = await testIsar.userModels.get(1);
|
||||
if (user != null) {
|
||||
user.subscriptionModel.value = subscription;
|
||||
await user.subscriptionModel.save();
|
||||
}
|
||||
});
|
||||
|
||||
// Reload user with subscription
|
||||
final userWithSubscription = await testIsar.userModels.get(1);
|
||||
expect(userWithSubscription, isNotNull);
|
||||
await userWithSubscription!.subscriptionModel.load();
|
||||
|
||||
final request = Request(
|
||||
'GET',
|
||||
Uri.parse('http://localhost/api/v2/subscriptions/status'),
|
||||
).change(context: {'user': userWithSubscription});
|
||||
|
||||
final response = await subscriptionsApiV2.getStatus(request);
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
expect(responseBody['active'], isTrue);
|
||||
expect(responseBody.containsKey('start'), isTrue);
|
||||
expect(responseBody.containsKey('finish'), isTrue);
|
||||
|
||||
final start = DateTime.parse(responseBody['start'] as String);
|
||||
final finish = DateTime.parse(responseBody['finish'] as String);
|
||||
|
||||
expect(start, equals(startDate));
|
||||
expect(finish, equals(finishDate));
|
||||
});
|
||||
|
||||
test('should return active: false when subscription is expired', () async {
|
||||
final now = DateTime.now();
|
||||
final startDate = now.subtract(const Duration(days: 35));
|
||||
final finishDate = now.subtract(const Duration(days: 5));
|
||||
|
||||
// Create expired subscription for test user
|
||||
await testIsar.writeTxn(() async {
|
||||
final subscription = UserSubscriptionModel(
|
||||
start: startDate,
|
||||
finish: finishDate,
|
||||
features: [SubscriptionFeatureEnum.packs],
|
||||
);
|
||||
await testIsar.userSubscriptionModels.put(subscription);
|
||||
|
||||
final user = await testIsar.userModels.get(1);
|
||||
if (user != null) {
|
||||
user.subscriptionModel.value = subscription;
|
||||
await user.subscriptionModel.save();
|
||||
}
|
||||
});
|
||||
|
||||
// Reload user with subscription
|
||||
final userWithSubscription = await testIsar.userModels.get(1);
|
||||
expect(userWithSubscription, isNotNull);
|
||||
await userWithSubscription!.subscriptionModel.load();
|
||||
|
||||
final request = Request(
|
||||
'GET',
|
||||
Uri.parse('http://localhost/api/v2/subscriptions/status'),
|
||||
).change(context: {'user': userWithSubscription});
|
||||
|
||||
final response = await subscriptionsApiV2.getStatus(request);
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
expect(responseBody['active'], isFalse);
|
||||
});
|
||||
|
||||
test('should return properly formatted JSON response', () async {
|
||||
final now = DateTime.now();
|
||||
final startDate = now.subtract(const Duration(days: 10));
|
||||
final finishDate = now.add(const Duration(days: 20));
|
||||
|
||||
// Create active subscription
|
||||
await testIsar.writeTxn(() async {
|
||||
final subscription = UserSubscriptionModel(
|
||||
start: startDate,
|
||||
finish: finishDate,
|
||||
features: [SubscriptionFeatureEnum.packs, SubscriptionFeatureEnum.ads],
|
||||
);
|
||||
await testIsar.userSubscriptionModels.put(subscription);
|
||||
|
||||
final user = await testIsar.userModels.get(1);
|
||||
if (user != null) {
|
||||
user.subscriptionModel.value = subscription;
|
||||
await user.subscriptionModel.save();
|
||||
}
|
||||
});
|
||||
|
||||
final userWithSubscription = await testIsar.userModels.get(1);
|
||||
expect(userWithSubscription, isNotNull);
|
||||
await userWithSubscription!.subscriptionModel.load();
|
||||
|
||||
final request = Request(
|
||||
'GET',
|
||||
Uri.parse('http://localhost/api/v2/subscriptions/status'),
|
||||
).change(context: {'user': userWithSubscription});
|
||||
|
||||
final response = await subscriptionsApiV2.getStatus(request);
|
||||
final responseBody = jsonDecode(await response.readAsString())
|
||||
as Map<String, dynamic>;
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
expect(response.headers['content-type'], contains('application/json'));
|
||||
expect(responseBody.containsKey('active'), isTrue);
|
||||
expect(responseBody['active'], isA<bool>());
|
||||
|
||||
if (responseBody['active'] as bool) {
|
||||
expect(responseBody.containsKey('start'), isTrue);
|
||||
expect(responseBody.containsKey('finish'), isTrue);
|
||||
expect(responseBody['start'], isA<String>());
|
||||
expect(responseBody['finish'], isA<String>());
|
||||
|
||||
// Verify ISO8601 format
|
||||
expect(
|
||||
() => DateTime.parse(responseBody['start'] as String),
|
||||
returnsNormally,
|
||||
);
|
||||
expect(
|
||||
() => DateTime.parse(responseBody['finish'] as String),
|
||||
returnsNormally,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue