diff --git a/ai_docs/agent/backend/agent_state.json b/ai_docs/agent/backend/agent_state.json index b3ae9b6..b997bf9 100644 --- a/ai_docs/agent/backend/agent_state.json +++ b/ai_docs/agent/backend/agent_state.json @@ -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, diff --git a/ai_docs/agent/backend/task_list.json b/ai_docs/agent/backend/task_list.json index dd11adc..4f3f770 100644 --- a/ai_docs/agent/backend/task_list.json +++ b/ai_docs/agent/backend/task_list.json @@ -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": [ diff --git a/mnemo_cards_backend/lib/api/v2/subscriptions_api_v2.dart b/mnemo_cards_backend/lib/api/v2/subscriptions_api_v2.dart index 3439fe3..c7db86e 100644 --- a/mnemo_cards_backend/lib/api/v2/subscriptions_api_v2.dart +++ b/mnemo_cards_backend/lib/api/v2/subscriptions_api_v2.dart @@ -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 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 diff --git a/mnemo_cards_backend/test/api/v2/subscriptions_api_v2_test.dart b/mnemo_cards_backend/test/api/v2/subscriptions_api_v2_test.dart index 7aeafe4..855c554 100644 --- a/mnemo_cards_backend/test/api/v2/subscriptions_api_v2_test.dart +++ b/mnemo_cards_backend/test/api/v2/subscriptions_api_v2_test.dart @@ -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; + + 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; + + 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; + + 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; + + 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; + + expect(response.statusCode, equals(200)); + expect(response.headers['content-type'], contains('application/json')); + expect(responseBody.containsKey('active'), isTrue); + expect(responseBody['active'], isA()); + + if (responseBody['active'] as bool) { + expect(responseBody.containsKey('start'), isTrue); + expect(responseBody.containsKey('finish'), isTrue); + expect(responseBody['start'], isA()); + expect(responseBody['finish'], isA()); + + // Verify ISO8601 format + expect( + () => DateTime.parse(responseBody['start'] as String), + returnsNormally, + ); + expect( + () => DateTime.parse(responseBody['finish'] as String), + returnsNormally, + ); + } + }); + }); }