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",
|
"component": "backend",
|
||||||
"current_task_id": "BACKEND-002",
|
"current_task_id": "BACKEND-005",
|
||||||
"iteration_count": 2,
|
"iteration_count": 3,
|
||||||
"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,
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,7 @@
|
||||||
"id": "BACKEND-005",
|
"id": "BACKEND-005",
|
||||||
"title": "Add Promocodes List Endpoint",
|
"title": "Add Promocodes List Endpoint",
|
||||||
"priority": "medium",
|
"priority": "medium",
|
||||||
"status": "pending",
|
"status": "in_progress",
|
||||||
"estimated_hours": 2.0,
|
"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.",
|
"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": [
|
"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
|
/// POST /api/v2/promocodes/{code}/apply
|
||||||
/// Applies promocode for current user.
|
/// Applies promocode for current user.
|
||||||
@Route.post('/promocodes/<code>/apply')
|
@Route.post('/promocodes/<code>/apply')
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,95 @@ class PromoCodesManager {
|
||||||
return dto;
|
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(
|
Future<void> createProductForAdsCampaignIfNeeded(
|
||||||
MnemoCardsProductModel product,
|
MnemoCardsProductModel product,
|
||||||
) async {
|
) 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