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",
|
"component": "backend",
|
||||||
"current_task_id": "BACKEND-001",
|
"current_task_id": "BACKEND-002",
|
||||||
"iteration_count": 1,
|
"iteration_count": 2,
|
||||||
"max_iterations": 10,
|
"max_iterations": 10,
|
||||||
"started_at": "2025-11-21T01:06:15.789515+00:00",
|
"started_at": "2025-11-21T01:06:15.789515+00:00",
|
||||||
"last_commit": null,
|
"last_commit": null,
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@
|
||||||
"id": "BACKEND-002",
|
"id": "BACKEND-002",
|
||||||
"title": "Implement Subscription Status Endpoint",
|
"title": "Implement Subscription Status Endpoint",
|
||||||
"priority": "high",
|
"priority": "high",
|
||||||
"status": "pending",
|
"status": "in_progress",
|
||||||
"estimated_hours": 2.0,
|
"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.",
|
"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": [
|
"acceptance_criteria": [
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import 'dart:convert';
|
||||||
import 'dart:developer' as developer;
|
import 'dart:developer' as developer;
|
||||||
|
|
||||||
import 'package:injectable/injectable.dart';
|
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:mnemo_cards_backend/api/subscription/subscription_manager.dart';
|
||||||
import 'package:shelf/shelf.dart';
|
import 'package:shelf/shelf.dart';
|
||||||
import 'package:shelf_open_api/shelf_open_api.dart';
|
import 'package:shelf_open_api/shelf_open_api.dart';
|
||||||
|
|
@ -40,6 +41,15 @@ class SubscriptionsApiV2 {
|
||||||
headers: {'Content-Type': 'application/json'},
|
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
|
/// GET /api/v2/subscriptions/plans
|
||||||
/// List available subscription plans
|
/// List available subscription plans
|
||||||
/// Returns all available subscription plans. Authentication is optional.
|
/// Returns all available subscription plans. Authentication is optional.
|
||||||
|
|
@ -75,8 +85,27 @@ class SubscriptionsApiV2 {
|
||||||
@Route.get('/subscriptions/status')
|
@Route.get('/subscriptions/status')
|
||||||
@OpenApiRoute()
|
@OpenApiRoute()
|
||||||
Future<Response> getStatus(Request request) async {
|
Future<Response> getStatus(Request request) async {
|
||||||
// TODO: implement
|
try {
|
||||||
return _ok({'active': false});
|
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
|
/// POST /api/v2/subscriptions/cancel
|
||||||
|
|
|
||||||
|
|
@ -301,4 +301,180 @@ void main() {
|
||||||
expect(paymentSystems, contains('google'));
|
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