feat(backend): Add Promocodes List Endpoint
Task ID: BACKEND-005 Priority: medium Changes: Completed by: AI Agent Duration: 182145ms
This commit is contained in:
parent
eabf5f354a
commit
849063096c
5 changed files with 662 additions and 3 deletions
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"component": "backend",
|
||||
"current_task_id": "BACKEND-002",
|
||||
"iteration_count": 2,
|
||||
"current_task_id": "BACKEND-005",
|
||||
"iteration_count": 3,
|
||||
"max_iterations": 10,
|
||||
"started_at": "2025-11-21T01:06:15.789515+00:00",
|
||||
"last_commit": null,
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@
|
|||
"id": "BACKEND-005",
|
||||
"title": "Add Promocodes List Endpoint",
|
||||
"priority": "medium",
|
||||
"status": "pending",
|
||||
"status": "in_progress",
|
||||
"estimated_hours": 2.0,
|
||||
"description": "Implement GET /api/v2/promocodes endpoint to list available promocodes for current user. Return active campaigns with their promocodes that user can apply. Filter by active status and date range.",
|
||||
"acceptance_criteria": [
|
||||
|
|
|
|||
|
|
@ -51,6 +51,23 @@ class PromocodesApiV2 {
|
|||
}
|
||||
}
|
||||
|
||||
/// GET /api/v2/promocodes
|
||||
/// Lists available promocodes for current user.
|
||||
/// Returns active campaigns with promocodes that user can apply.
|
||||
@Route.get('/promocodes')
|
||||
Future<Response> listPromocodes(Request request) async {
|
||||
final user = request.user;
|
||||
if (user == null) {
|
||||
return _unauthorized();
|
||||
}
|
||||
|
||||
final campaigns =
|
||||
await _promoCodesManager.listAvailablePromocodes(user);
|
||||
return _json({
|
||||
'campaigns': campaigns.map((c) => c.toJson()).toList(),
|
||||
});
|
||||
}
|
||||
|
||||
/// POST /api/v2/promocodes/{code}/apply
|
||||
/// Applies promocode for current user.
|
||||
@Route.post('/promocodes/<code>/apply')
|
||||
|
|
|
|||
|
|
@ -36,6 +36,95 @@ class PromoCodesManager {
|
|||
return dto;
|
||||
}
|
||||
|
||||
/// Lists available promocodes for a user.
|
||||
/// Returns active campaigns with promocodes that the user can apply.
|
||||
Future<List<PromoCodesCampaignDto>> listAvailablePromocodes(
|
||||
UserModel user,
|
||||
) async {
|
||||
await user.userData.load();
|
||||
final userData = user.userData.value;
|
||||
if (userData == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
await userData.activatedPromoCodes.load();
|
||||
final activatedPromoCodes = userData.activatedPromoCodes;
|
||||
|
||||
final now = DateTime.now();
|
||||
|
||||
// Get all active campaigns within date range
|
||||
// Campaign is active if: now >= start && now <= finish
|
||||
final activeCampaigns = await isar.txn(() async {
|
||||
return await isar.promoCodesCampaignModels
|
||||
.filter()
|
||||
.statusEqualTo(PromoCodeCampaignModelStatus.active)
|
||||
.startLessThan(now, include: true)
|
||||
.finishGreaterThan(now, include: true)
|
||||
.findAll();
|
||||
});
|
||||
|
||||
final availableCampaigns = <PromoCodesCampaignDto>[];
|
||||
|
||||
for (final campaign in activeCampaigns) {
|
||||
// Check if campaign has tags and user matches them
|
||||
if (campaign.tags.isNotEmpty &&
|
||||
!campaign.tags.hasIntersection(userData.tags)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if user has already reached activationsPerUser limit
|
||||
final userActivationsForCampaign = activatedPromoCodes
|
||||
.where((p) => p.campaign.value?.id == campaign.id)
|
||||
.length;
|
||||
if (userActivationsForCampaign >= campaign.activationsPerUser) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Load promocodes for this campaign
|
||||
await campaign.promoCodes.load();
|
||||
final campaignPromoCodes = campaign.promoCodes.toList();
|
||||
|
||||
// Filter available promocodes:
|
||||
// - Not already activated by user
|
||||
// - Not reached activationsPerCode limit
|
||||
// - Not individual promocodes for other users
|
||||
final availableCodes = <String>[];
|
||||
for (final promoCode in campaignPromoCodes) {
|
||||
// Skip if already activated by this user
|
||||
if (activatedPromoCodes.contains(promoCode)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip if reached activation limit
|
||||
if (promoCode.activations >= campaign.activationsPerCode) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if it's an individual promocode for another user
|
||||
await promoCode.userData.load();
|
||||
final codeUserData = promoCode.userData.value;
|
||||
if (codeUserData != null) {
|
||||
await codeUserData.user.load();
|
||||
if (codeUserData.user.value?.id != user.id) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
availableCodes.add(promoCode.code);
|
||||
}
|
||||
|
||||
// Only include campaign if it has available codes
|
||||
if (availableCodes.isNotEmpty) {
|
||||
final dto = await campaign.toDto(withCodes: false);
|
||||
availableCampaigns.add(
|
||||
dto.copyWith(promoCodes: availableCodes),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return availableCampaigns;
|
||||
}
|
||||
|
||||
Future<void> createProductForAdsCampaignIfNeeded(
|
||||
MnemoCardsProductModel product,
|
||||
) async {
|
||||
|
|
|
|||
553
mnemo_cards_backend/test/api/v2/promocodes_api_v2_test.dart
Normal file
553
mnemo_cards_backend/test/api/v2/promocodes_api_v2_test.dart
Normal file
|
|
@ -0,0 +1,553 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:mnemo_cards_backend/api/ads/ads_manager.dart';
|
||||
import 'package:mnemo_cards_backend/api/purchase/payment_manager.dart';
|
||||
import 'package:mnemo_cards_backend/api/purchase/rustore/rustore_purchase_handler.dart';
|
||||
import 'package:mnemo_cards_backend/api/purchase/yoo_money.dart';
|
||||
import 'package:mnemo_cards_backend/api/subscription/subscription_manager.dart';
|
||||
import 'package:mnemo_cards_backend/api/v2/promocodes_api_v2.dart';
|
||||
import 'package:mnemo_cards_backend/discounts/discounts_manager.dart';
|
||||
import 'package:mnemo_cards_backend/main.dart' as backend_main;
|
||||
import 'package:mnemo_cards_backend/packs/pack_dto_converter.dart';
|
||||
import 'package:mnemo_cards_backend/packs/pack_manager.dart';
|
||||
import 'package:mnemo_cards_backend/packs/products_price_resolver.dart';
|
||||
import 'package:mnemo_cards_backend/promo_codes/promo_codes_manager.dart';
|
||||
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
|
||||
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
||||
import 'package:shelf/shelf.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
late Isar testIsar;
|
||||
late PromoCodesManager promoCodesManager;
|
||||
late PromocodesApiV2 promocodesApiV2;
|
||||
late UserModel testUser;
|
||||
late UserDataModel testUserData;
|
||||
|
||||
Request buildRequest(
|
||||
String method,
|
||||
String url, {
|
||||
UserModel? user,
|
||||
}) {
|
||||
final context = <String, Object?>{};
|
||||
|
||||
if (user != null) {
|
||||
context['user'] = user;
|
||||
}
|
||||
|
||||
final uri = url.startsWith('http') ? Uri.parse(url) : Uri.parse('http://localhost$url');
|
||||
return Request(method, uri).change(context: context);
|
||||
}
|
||||
|
||||
setUpAll(() async {
|
||||
// Initialize Isar for testing
|
||||
await Isar.initializeIsarCore(download: true);
|
||||
final testDir = Directory.systemTemp.createTempSync('isar_test_');
|
||||
|
||||
testIsar = await Isar.open(
|
||||
[
|
||||
CardPackModelSchema,
|
||||
GameCardModelSchema,
|
||||
UserModelSchema,
|
||||
UserSubscriptionModelSchema,
|
||||
SubscriptionPlanModelSchema,
|
||||
TokenModelSchema,
|
||||
RefreshTokenModelSchema,
|
||||
TestModelSchema,
|
||||
TestQuestionModelSchema,
|
||||
PaymentModelSchema,
|
||||
TaskModelSchema,
|
||||
TestStatisticsModelSchema,
|
||||
UserDataModelSchema,
|
||||
PromoCodesCampaignModelSchema,
|
||||
PromoCodeModelSchema,
|
||||
DiscountCampaignModelSchema,
|
||||
DiscountModelSchema,
|
||||
],
|
||||
directory: testDir.path,
|
||||
name: 'test_db',
|
||||
inspector: false,
|
||||
);
|
||||
|
||||
backend_main.isar = testIsar;
|
||||
});
|
||||
|
||||
setUp(() async {
|
||||
// Set up dependencies
|
||||
final discountsManager = const DiscountsManager();
|
||||
final productsPriceResolver = ProductsPriceResolver(discountsManager);
|
||||
final adsManager = AdsManager();
|
||||
final packDtoConverter = PackDtoConverter(
|
||||
productsPriceResolver,
|
||||
adsManager,
|
||||
);
|
||||
final packManager = PackManager(packDtoConverter);
|
||||
final subscriptionManager = SubscriptionManager();
|
||||
final yooMoneyHandler = YooMoneyHandler(packManager);
|
||||
final rustorePurchaseHandler = RustorePurchaseHandler();
|
||||
final paymentManager = PaymentManager(
|
||||
packManager,
|
||||
subscriptionManager,
|
||||
yooMoneyHandler,
|
||||
rustorePurchaseHandler,
|
||||
productsPriceResolver,
|
||||
);
|
||||
promoCodesManager = PromoCodesManager(paymentManager);
|
||||
promocodesApiV2 = PromocodesApiV2(promoCodesManager);
|
||||
|
||||
// Create test user with userData
|
||||
await testIsar.writeTxn(() async {
|
||||
final user = UserModel.empty.copyWith(
|
||||
id: 1,
|
||||
name: 'Test User',
|
||||
email: 'test@example.com',
|
||||
);
|
||||
await testIsar.userModels.put(user);
|
||||
testUser = user;
|
||||
|
||||
final userData = UserDataModel(
|
||||
tags: ['premium', 'beta'],
|
||||
words: [],
|
||||
);
|
||||
await testIsar.userDataModels.put(userData);
|
||||
userData.user.value = user;
|
||||
await userData.user.save();
|
||||
testUser.userData.value = userData;
|
||||
await testUser.userData.save();
|
||||
testUserData = userData;
|
||||
});
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
// Clean up test data
|
||||
await testIsar.writeTxn(() async {
|
||||
await testIsar.promoCodeModels.clear();
|
||||
await testIsar.promoCodesCampaignModels.clear();
|
||||
await testIsar.userDataModels.clear();
|
||||
await testIsar.userModels.clear();
|
||||
await testIsar.paymentModels.clear();
|
||||
});
|
||||
});
|
||||
|
||||
group('GET /api/v2/promocodes', () {
|
||||
test('returns 401 for unauthenticated requests', () async {
|
||||
final request = buildRequest('GET', '/api/v2/promocodes');
|
||||
final response = await promocodesApiV2.listPromocodes(request);
|
||||
|
||||
expect(response.statusCode, equals(401));
|
||||
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
expect(body['error'], equals('unauthorized'));
|
||||
});
|
||||
|
||||
test('returns empty list when no campaigns available', () async {
|
||||
final request = buildRequest('GET', '/api/v2/promocodes', user: testUser);
|
||||
final response = await promocodesApiV2.listPromocodes(request);
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
expect(body['campaigns'], isA<List>());
|
||||
expect((body['campaigns'] as List).isEmpty, isTrue);
|
||||
});
|
||||
|
||||
test('returns active campaigns within date range', () async {
|
||||
final now = DateTime.now();
|
||||
final start = now.subtract(const Duration(days: 1));
|
||||
final finish = now.add(const Duration(days: 1));
|
||||
|
||||
// Create active campaign
|
||||
await testIsar.writeTxn(() async {
|
||||
final campaign = PromoCodesCampaignModel(
|
||||
template: 'TESTXXXX',
|
||||
products: [],
|
||||
activationsPerCode: 10,
|
||||
activationsPerUser: 1,
|
||||
generationSize: 100,
|
||||
start: start,
|
||||
finish: finish,
|
||||
status: PromoCodeCampaignModelStatus.active,
|
||||
tags: [],
|
||||
);
|
||||
await testIsar.promoCodesCampaignModels.put(campaign);
|
||||
|
||||
// Create promocodes
|
||||
final promoCode1 = PromoCodeModel(code: 'TEST1234');
|
||||
final promoCode2 = PromoCodeModel(code: 'TEST5678');
|
||||
await testIsar.promoCodeModels.putAll([promoCode1, promoCode2]);
|
||||
|
||||
campaign.promoCodes.addAll([promoCode1, promoCode2]);
|
||||
await campaign.promoCodes.save();
|
||||
});
|
||||
|
||||
final request = buildRequest('GET', '/api/v2/promocodes', user: testUser);
|
||||
final response = await promocodesApiV2.listPromocodes(request);
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
expect(body['campaigns'], isA<List>());
|
||||
expect((body['campaigns'] as List).length, equals(1));
|
||||
|
||||
final campaign = (body['campaigns'] as List).first as Map<String, dynamic>;
|
||||
expect(campaign['status'], equals('active'));
|
||||
expect(campaign['promoCodes'], isA<List>());
|
||||
expect((campaign['promoCodes'] as List).length, equals(2));
|
||||
});
|
||||
|
||||
test('filters out inactive campaigns', () async {
|
||||
final now = DateTime.now();
|
||||
final start = now.subtract(const Duration(days: 1));
|
||||
final finish = now.add(const Duration(days: 1));
|
||||
|
||||
await testIsar.writeTxn(() async {
|
||||
// Create inactive campaign
|
||||
final inactiveCampaign = PromoCodesCampaignModel(
|
||||
template: 'INACTIVE',
|
||||
products: [],
|
||||
activationsPerCode: 10,
|
||||
activationsPerUser: 1,
|
||||
generationSize: 100,
|
||||
start: start,
|
||||
finish: finish,
|
||||
status: PromoCodeCampaignModelStatus.disabled,
|
||||
tags: [],
|
||||
);
|
||||
await testIsar.promoCodesCampaignModels.put(inactiveCampaign);
|
||||
|
||||
// Create active campaign
|
||||
final activeCampaign = PromoCodesCampaignModel(
|
||||
template: 'ACTIVE',
|
||||
products: [],
|
||||
activationsPerCode: 10,
|
||||
activationsPerUser: 1,
|
||||
generationSize: 100,
|
||||
start: start,
|
||||
finish: finish,
|
||||
status: PromoCodeCampaignModelStatus.active,
|
||||
tags: [],
|
||||
);
|
||||
await testIsar.promoCodesCampaignModels.put(activeCampaign);
|
||||
|
||||
final promoCode = PromoCodeModel(code: 'ACTIVE123');
|
||||
await testIsar.promoCodeModels.put(promoCode);
|
||||
activeCampaign.promoCodes.add(promoCode);
|
||||
await activeCampaign.promoCodes.save();
|
||||
});
|
||||
|
||||
final request = buildRequest('GET', '/api/v2/promocodes', user: testUser);
|
||||
final response = await promocodesApiV2.listPromocodes(request);
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
final campaigns = body['campaigns'] as List;
|
||||
expect(campaigns.length, equals(1));
|
||||
expect((campaigns.first as Map<String, dynamic>)['template'], equals('ACTIVE'));
|
||||
});
|
||||
|
||||
test('filters out campaigns outside date range', () async {
|
||||
final now = DateTime.now();
|
||||
|
||||
await testIsar.writeTxn(() async {
|
||||
// Campaign that hasn't started
|
||||
final futureCampaign = PromoCodesCampaignModel(
|
||||
template: 'FUTURE',
|
||||
products: [],
|
||||
activationsPerCode: 10,
|
||||
activationsPerUser: 1,
|
||||
generationSize: 100,
|
||||
start: now.add(const Duration(days: 1)),
|
||||
finish: now.add(const Duration(days: 2)),
|
||||
status: PromoCodeCampaignModelStatus.active,
|
||||
tags: [],
|
||||
);
|
||||
await testIsar.promoCodesCampaignModels.put(futureCampaign);
|
||||
|
||||
// Campaign that has finished
|
||||
final pastCampaign = PromoCodesCampaignModel(
|
||||
template: 'PAST',
|
||||
products: [],
|
||||
activationsPerCode: 10,
|
||||
activationsPerUser: 1,
|
||||
generationSize: 100,
|
||||
start: now.subtract(const Duration(days: 2)),
|
||||
finish: now.subtract(const Duration(days: 1)),
|
||||
status: PromoCodeCampaignModelStatus.active,
|
||||
tags: [],
|
||||
);
|
||||
await testIsar.promoCodesCampaignModels.put(pastCampaign);
|
||||
|
||||
// Active campaign within date range
|
||||
final activeCampaign = PromoCodesCampaignModel(
|
||||
template: 'ACTIVE',
|
||||
products: [],
|
||||
activationsPerCode: 10,
|
||||
activationsPerUser: 1,
|
||||
generationSize: 100,
|
||||
start: now.subtract(const Duration(days: 1)),
|
||||
finish: now.add(const Duration(days: 1)),
|
||||
status: PromoCodeCampaignModelStatus.active,
|
||||
tags: [],
|
||||
);
|
||||
await testIsar.promoCodesCampaignModels.put(activeCampaign);
|
||||
|
||||
final promoCode = PromoCodeModel(code: 'ACTIVE123');
|
||||
await testIsar.promoCodeModels.put(promoCode);
|
||||
activeCampaign.promoCodes.add(promoCode);
|
||||
await activeCampaign.promoCodes.save();
|
||||
});
|
||||
|
||||
final request = buildRequest('GET', '/api/v2/promocodes', user: testUser);
|
||||
final response = await promocodesApiV2.listPromocodes(request);
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
final campaigns = body['campaigns'] as List;
|
||||
expect(campaigns.length, equals(1));
|
||||
expect((campaigns.first as Map<String, dynamic>)['template'], equals('ACTIVE'));
|
||||
});
|
||||
|
||||
test('filters campaigns by user tags', () async {
|
||||
final now = DateTime.now();
|
||||
final start = now.subtract(const Duration(days: 1));
|
||||
final finish = now.add(const Duration(days: 1));
|
||||
|
||||
await testIsar.writeTxn(() async {
|
||||
// Campaign with matching tags
|
||||
final matchingCampaign = PromoCodesCampaignModel(
|
||||
template: 'MATCHING',
|
||||
products: [],
|
||||
activationsPerCode: 10,
|
||||
activationsPerUser: 1,
|
||||
generationSize: 100,
|
||||
start: start,
|
||||
finish: finish,
|
||||
status: PromoCodeCampaignModelStatus.active,
|
||||
tags: ['premium'],
|
||||
);
|
||||
await testIsar.promoCodesCampaignModels.put(matchingCampaign);
|
||||
|
||||
// Campaign with non-matching tags
|
||||
final nonMatchingCampaign = PromoCodesCampaignModel(
|
||||
template: 'NONMATCHING',
|
||||
products: [],
|
||||
activationsPerCode: 10,
|
||||
activationsPerUser: 1,
|
||||
generationSize: 100,
|
||||
start: start,
|
||||
finish: finish,
|
||||
status: PromoCodeCampaignModelStatus.active,
|
||||
tags: ['vip'],
|
||||
);
|
||||
await testIsar.promoCodesCampaignModels.put(nonMatchingCampaign);
|
||||
|
||||
// Campaign without tags (should be available)
|
||||
final noTagsCampaign = PromoCodesCampaignModel(
|
||||
template: 'NOTAGS',
|
||||
products: [],
|
||||
activationsPerCode: 10,
|
||||
activationsPerUser: 1,
|
||||
generationSize: 100,
|
||||
start: start,
|
||||
finish: finish,
|
||||
status: PromoCodeCampaignModelStatus.active,
|
||||
tags: [],
|
||||
);
|
||||
await testIsar.promoCodesCampaignModels.put(noTagsCampaign);
|
||||
|
||||
final promoCode1 = PromoCodeModel(code: 'MATCH123');
|
||||
final promoCode2 = PromoCodeModel(code: 'NOTAGS123');
|
||||
await testIsar.promoCodeModels.putAll([promoCode1, promoCode2]);
|
||||
|
||||
matchingCampaign.promoCodes.add(promoCode1);
|
||||
noTagsCampaign.promoCodes.add(promoCode2);
|
||||
await matchingCampaign.promoCodes.save();
|
||||
await noTagsCampaign.promoCodes.save();
|
||||
});
|
||||
|
||||
final request = buildRequest('GET', '/api/v2/promocodes', user: testUser);
|
||||
final response = await promocodesApiV2.listPromocodes(request);
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
final campaigns = body['campaigns'] as List;
|
||||
expect(campaigns.length, equals(2));
|
||||
final templates = campaigns.map((c) => (c as Map<String, dynamic>)['template']).toList();
|
||||
expect(templates, contains('MATCHING'));
|
||||
expect(templates, contains('NOTAGS'));
|
||||
expect(templates, isNot(contains('NONMATCHING')));
|
||||
});
|
||||
|
||||
test('filters out campaigns where user reached activation limit', () async {
|
||||
final now = DateTime.now();
|
||||
final start = now.subtract(const Duration(days: 1));
|
||||
final finish = now.add(const Duration(days: 1));
|
||||
|
||||
await testIsar.writeTxn(() async {
|
||||
final campaign = PromoCodesCampaignModel(
|
||||
template: 'LIMITED',
|
||||
products: [],
|
||||
activationsPerCode: 10,
|
||||
activationsPerUser: 1, // User can only activate once
|
||||
generationSize: 100,
|
||||
start: start,
|
||||
finish: finish,
|
||||
status: PromoCodeCampaignModelStatus.active,
|
||||
tags: [],
|
||||
);
|
||||
await testIsar.promoCodesCampaignModels.put(campaign);
|
||||
|
||||
final promoCode = PromoCodeModel(code: 'LIMITED123');
|
||||
await testIsar.promoCodeModels.put(promoCode);
|
||||
campaign.promoCodes.add(promoCode);
|
||||
await campaign.promoCodes.save();
|
||||
|
||||
// User has already activated a promocode from this campaign
|
||||
testUserData.activatedPromoCodes.add(promoCode);
|
||||
await testUserData.activatedPromoCodes.save();
|
||||
await testIsar.userDataModels.put(testUserData);
|
||||
});
|
||||
|
||||
final request = buildRequest('GET', '/api/v2/promocodes', user: testUser);
|
||||
final response = await promocodesApiV2.listPromocodes(request);
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
final campaigns = body['campaigns'] as List;
|
||||
expect(campaigns.isEmpty, isTrue);
|
||||
});
|
||||
|
||||
test('filters out promocodes that reached activation limit', () async {
|
||||
final now = DateTime.now();
|
||||
final start = now.subtract(const Duration(days: 1));
|
||||
final finish = now.add(const Duration(days: 1));
|
||||
|
||||
await testIsar.writeTxn(() async {
|
||||
final campaign = PromoCodesCampaignModel(
|
||||
template: 'LIMITED',
|
||||
products: [],
|
||||
activationsPerCode: 1, // Code can only be activated once
|
||||
activationsPerUser: 10,
|
||||
generationSize: 100,
|
||||
start: start,
|
||||
finish: finish,
|
||||
status: PromoCodeCampaignModelStatus.active,
|
||||
tags: [],
|
||||
);
|
||||
await testIsar.promoCodesCampaignModels.put(campaign);
|
||||
|
||||
final promoCode1 = PromoCodeModel(code: 'LIMITED1', activations: 1); // Already used
|
||||
final promoCode2 = PromoCodeModel(code: 'LIMITED2', activations: 0); // Available
|
||||
await testIsar.promoCodeModels.putAll([promoCode1, promoCode2]);
|
||||
|
||||
campaign.promoCodes.addAll([promoCode1, promoCode2]);
|
||||
await campaign.promoCodes.save();
|
||||
});
|
||||
|
||||
final request = buildRequest('GET', '/api/v2/promocodes', user: testUser);
|
||||
final response = await promocodesApiV2.listPromocodes(request);
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
final campaigns = body['campaigns'] as List;
|
||||
expect(campaigns.length, equals(1));
|
||||
|
||||
final campaign = campaigns.first as Map<String, dynamic>;
|
||||
final promoCodes = campaign['promoCodes'] as List;
|
||||
expect(promoCodes.length, equals(1));
|
||||
expect(promoCodes.first, equals('LIMITED2'));
|
||||
});
|
||||
|
||||
test('filters out promocodes already activated by user', () async {
|
||||
final now = DateTime.now();
|
||||
final start = now.subtract(const Duration(days: 1));
|
||||
final finish = now.add(const Duration(days: 1));
|
||||
|
||||
await testIsar.writeTxn(() async {
|
||||
final campaign = PromoCodesCampaignModel(
|
||||
template: 'MULTI',
|
||||
products: [],
|
||||
activationsPerCode: 10,
|
||||
activationsPerUser: 10,
|
||||
generationSize: 100,
|
||||
start: start,
|
||||
finish: finish,
|
||||
status: PromoCodeCampaignModelStatus.active,
|
||||
tags: [],
|
||||
);
|
||||
await testIsar.promoCodesCampaignModels.put(campaign);
|
||||
|
||||
final promoCode1 = PromoCodeModel(code: 'USED1');
|
||||
final promoCode2 = PromoCodeModel(code: 'AVAILABLE1');
|
||||
await testIsar.promoCodeModels.putAll([promoCode1, promoCode2]);
|
||||
|
||||
campaign.promoCodes.addAll([promoCode1, promoCode2]);
|
||||
await campaign.promoCodes.save();
|
||||
|
||||
// User has already activated promoCode1
|
||||
testUserData.activatedPromoCodes.add(promoCode1);
|
||||
await testUserData.activatedPromoCodes.save();
|
||||
await testIsar.userDataModels.put(testUserData);
|
||||
});
|
||||
|
||||
final request = buildRequest('GET', '/api/v2/promocodes', user: testUser);
|
||||
final response = await promocodesApiV2.listPromocodes(request);
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
final campaigns = body['campaigns'] as List;
|
||||
expect(campaigns.length, equals(1));
|
||||
|
||||
final campaign = campaigns.first as Map<String, dynamic>;
|
||||
final promoCodes = campaign['promoCodes'] as List;
|
||||
expect(promoCodes.length, equals(1));
|
||||
expect(promoCodes.first, equals('AVAILABLE1'));
|
||||
});
|
||||
|
||||
test('returns proper JSON format with campaign and code information', () async {
|
||||
final now = DateTime.now();
|
||||
final start = now.subtract(const Duration(days: 1));
|
||||
final finish = now.add(const Duration(days: 1));
|
||||
|
||||
await testIsar.writeTxn(() async {
|
||||
final campaign = PromoCodesCampaignModel(
|
||||
template: 'FORMAT',
|
||||
products: [],
|
||||
activationsPerCode: 10,
|
||||
activationsPerUser: 1,
|
||||
generationSize: 100,
|
||||
start: start,
|
||||
finish: finish,
|
||||
status: PromoCodeCampaignModelStatus.active,
|
||||
tags: [],
|
||||
name: 'Test Campaign',
|
||||
);
|
||||
await testIsar.promoCodesCampaignModels.put(campaign);
|
||||
|
||||
final promoCode = PromoCodeModel(code: 'FORMAT123');
|
||||
await testIsar.promoCodeModels.put(promoCode);
|
||||
campaign.promoCodes.add(promoCode);
|
||||
await campaign.promoCodes.save();
|
||||
});
|
||||
|
||||
final request = buildRequest('GET', '/api/v2/promocodes', user: testUser);
|
||||
final response = await promocodesApiV2.listPromocodes(request);
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
expect(body.containsKey('campaigns'), isTrue);
|
||||
|
||||
final campaigns = body['campaigns'] as List;
|
||||
expect(campaigns.length, equals(1));
|
||||
|
||||
final campaign = campaigns.first as Map<String, dynamic>;
|
||||
expect(campaign.containsKey('id'), isTrue);
|
||||
expect(campaign.containsKey('name'), isTrue);
|
||||
expect(campaign.containsKey('status'), isTrue);
|
||||
expect(campaign.containsKey('start'), isTrue);
|
||||
expect(campaign.containsKey('finish'), isTrue);
|
||||
expect(campaign.containsKey('promoCodes'), isTrue);
|
||||
expect(campaign['status'], equals('active'));
|
||||
expect(campaign['promoCodes'], isA<List>());
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
Reference in a new issue