feat(backend): Add Promocode Validation Endpoint
Task ID: BACKEND-006 Priority: medium Changes: Completed by: AI Agent Duration: 144574ms
This commit is contained in:
parent
849063096c
commit
840e453255
7 changed files with 1067 additions and 333 deletions
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"component": "backend",
|
||||
"current_task_id": "BACKEND-005",
|
||||
"iteration_count": 3,
|
||||
"current_task_id": "BACKEND-006",
|
||||
"iteration_count": 4,
|
||||
"max_iterations": 10,
|
||||
"started_at": "2025-11-21T01:06:15.789515+00:00",
|
||||
"last_commit": null,
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@
|
|||
"id": "BACKEND-006",
|
||||
"title": "Add Promocode Validation Endpoint",
|
||||
"priority": "medium",
|
||||
"status": "pending",
|
||||
"status": "in_progress",
|
||||
"estimated_hours": 2.0,
|
||||
"description": "Implement GET /api/v2/promocodes/{code}/validate endpoint to validate a promocode without applying it. Check if code exists, is active, hasn't exceeded activation limits, and user is eligible.",
|
||||
"acceptance_criteria": [
|
||||
|
|
|
|||
|
|
@ -68,6 +68,45 @@ class PromocodesApiV2 {
|
|||
});
|
||||
}
|
||||
|
||||
/// GET /api/v2/promocodes/{code}/validate
|
||||
/// Validates a promocode without applying it.
|
||||
/// Returns validation result with valid: true/false and message.
|
||||
@Route.get('/promocodes/<code>/validate')
|
||||
Future<Response> validatePromocode(
|
||||
Request request,
|
||||
String code,
|
||||
) async {
|
||||
final user = request.user;
|
||||
if (user == null) {
|
||||
return _unauthorized();
|
||||
}
|
||||
|
||||
if (code.isEmpty) {
|
||||
return _json(
|
||||
{
|
||||
'error': 'bad_request',
|
||||
'message': 'Promocode is required',
|
||||
},
|
||||
statusCode: 400,
|
||||
);
|
||||
}
|
||||
|
||||
final result = await _promoCodesManager.validatePromocode(code, user);
|
||||
|
||||
// Return 404 if code not found
|
||||
if (!result['valid'] && result['message'] == 'Промокод не найден') {
|
||||
return _json(
|
||||
{
|
||||
'valid': false,
|
||||
'message': result['message'],
|
||||
},
|
||||
statusCode: 404,
|
||||
);
|
||||
}
|
||||
|
||||
return _json(result);
|
||||
}
|
||||
|
||||
/// POST /api/v2/promocodes/{code}/apply
|
||||
/// Applies promocode for current user.
|
||||
@Route.post('/promocodes/<code>/apply')
|
||||
|
|
|
|||
|
|
@ -8,6 +8,16 @@ part of 'promocodes_api_v2.dart';
|
|||
|
||||
Router _$PromocodesApiV2Router(PromocodesApiV2 service) {
|
||||
final router = Router();
|
||||
router.add(
|
||||
'GET',
|
||||
r'/promocodes',
|
||||
service.listPromocodes,
|
||||
);
|
||||
router.add(
|
||||
'GET',
|
||||
r'/promocodes/<code>/validate',
|
||||
service.validatePromocode,
|
||||
);
|
||||
router.add(
|
||||
'POST',
|
||||
r'/promocodes/<code>/apply',
|
||||
|
|
|
|||
|
|
@ -215,6 +215,130 @@ class PromoCodesManager {
|
|||
return true;
|
||||
}
|
||||
|
||||
/// Validates a promocode without applying it.
|
||||
/// Returns a map with 'valid' (bool) and 'message' (String) keys.
|
||||
Future<Map<String, dynamic>> validatePromocode(
|
||||
String code,
|
||||
UserModel user,
|
||||
) async {
|
||||
try {
|
||||
final result = await isar.txn(() async {
|
||||
final upperCode = code.toUpperCase();
|
||||
|
||||
// Check if code exists
|
||||
final codeModel =
|
||||
await isar.promoCodeModels.filter().codeEqualTo(upperCode).findFirst();
|
||||
await codeModel?.campaign.load();
|
||||
final campaign = codeModel?.campaign.value;
|
||||
if (codeModel == null || campaign == null) {
|
||||
return {
|
||||
'valid': false,
|
||||
'message': 'Промокод не найден',
|
||||
};
|
||||
}
|
||||
|
||||
// Check campaign status and date range
|
||||
final now = DateTime.now();
|
||||
if (campaign.status != PromoCodeCampaignModelStatus.active) {
|
||||
return {
|
||||
'valid': false,
|
||||
'message': 'Промокод недействителен',
|
||||
};
|
||||
}
|
||||
if (now.isBefore(campaign.start)) {
|
||||
return {
|
||||
'valid': false,
|
||||
'message': 'Промокод еще не активен',
|
||||
};
|
||||
}
|
||||
if (now.isAfter(campaign.finish)) {
|
||||
return {
|
||||
'valid': false,
|
||||
'message': 'Промокод истек',
|
||||
};
|
||||
}
|
||||
|
||||
// Check if code has reached activation limit
|
||||
if (codeModel.activations >= campaign.activationsPerCode) {
|
||||
return {
|
||||
'valid': false,
|
||||
'message': 'Промокод исчерпан',
|
||||
};
|
||||
}
|
||||
|
||||
// Load user data
|
||||
await user.userData.load();
|
||||
final userData = user.userData.value;
|
||||
if (userData == null) {
|
||||
return {
|
||||
'valid': false,
|
||||
'message': 'Ошибка при проверке промокода',
|
||||
};
|
||||
}
|
||||
|
||||
// Check if user has already activated this code
|
||||
await userData.activatedPromoCodes.load();
|
||||
final activatedPromoCodes = userData.activatedPromoCodes;
|
||||
if (activatedPromoCodes.contains(codeModel)) {
|
||||
return {
|
||||
'valid': false,
|
||||
'message': 'Промокод уже был активирован',
|
||||
};
|
||||
}
|
||||
|
||||
// Check if user has reached activation limit for this campaign
|
||||
// Load campaign relationships for activated promocodes
|
||||
for (final activatedCode in activatedPromoCodes) {
|
||||
await activatedCode.campaign.load();
|
||||
}
|
||||
if (activatedPromoCodes
|
||||
.where((p) => p.campaign.value?.id == campaign.id)
|
||||
.length >=
|
||||
campaign.activationsPerUser) {
|
||||
return {
|
||||
'valid': false,
|
||||
'message': 'Вы уже участвовали в этой акции',
|
||||
};
|
||||
}
|
||||
|
||||
// Check user tags match campaign tags
|
||||
if (campaign.tags.isNotEmpty &&
|
||||
!campaign.tags.hasIntersection(userData.tags)) {
|
||||
return {
|
||||
'valid': false,
|
||||
'message': 'Промокод недействителен',
|
||||
};
|
||||
}
|
||||
|
||||
// Check if it's an individual promocode for another user
|
||||
await codeModel.userData.load();
|
||||
final codeUserData = codeModel.userData.value;
|
||||
if (codeUserData != null) {
|
||||
await codeUserData.user.load();
|
||||
if (codeUserData.user.value?.id != user.id) {
|
||||
return {
|
||||
'valid': false,
|
||||
'message': 'Это промокод для другого пользователя',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// All checks passed
|
||||
return {
|
||||
'valid': true,
|
||||
'message': 'Промокод действителен',
|
||||
};
|
||||
});
|
||||
return result;
|
||||
} catch (e, s) {
|
||||
log('Error when validating promo code', error: e, stackTrace: s);
|
||||
return {
|
||||
'valid': false,
|
||||
'message': 'Ошибка при проверке промокода',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Future<PromoCodeDto> applyPromoCode(PromoCodeDto dto, UserModel user) async {
|
||||
try {
|
||||
PromoCodeModel? checkedCodeModel;
|
||||
|
|
|
|||
|
|
@ -3,17 +3,17 @@ info:
|
|||
title: Api
|
||||
version: 0.0.0
|
||||
servers:
|
||||
- url: "https://api.mnemo-cards.online"
|
||||
- url: "http://localhost:8080"
|
||||
paths:
|
||||
/promocodes/<code>/apply:
|
||||
/purchases/packs/<packId>:
|
||||
post:
|
||||
tags:
|
||||
- PromocodesApiV2
|
||||
summary: applyPromocode
|
||||
description: "POST /api/v2/promocodes/{code}/apply\nApplies promocode for current user."
|
||||
operationId: applyPromocode
|
||||
- PurchasesApiV2
|
||||
summary: createPackPurchase
|
||||
description: "POST /api/v2/purchases/packs/{packId}\nCreate purchase intent for a pack\nReturns purchase info including payment URL for YooKassa"
|
||||
operationId: createPackPurchase
|
||||
parameters:
|
||||
- name: code
|
||||
- name: packId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
|
|
@ -21,34 +21,15 @@ paths:
|
|||
responses:
|
||||
200:
|
||||
description: "Operation completed!"
|
||||
/admin/promocodes:
|
||||
/purchases/packs/<packId>/status:
|
||||
get:
|
||||
tags:
|
||||
- PromocodesApiV2
|
||||
summary: listPromoCodeCampaigns
|
||||
description: GET /api/v2/admin/promocodes\nLists promocode campaigns (admin only).
|
||||
operationId: listPromoCodeCampaigns
|
||||
responses:
|
||||
200:
|
||||
description: "Operation completed!"
|
||||
post:
|
||||
tags:
|
||||
- PromocodesApiV2
|
||||
summary: upsertPromoCodeCampaign
|
||||
description: POST /api/v2/admin/promocodes\nCreates or updates promocode campaign (admin only).
|
||||
operationId: upsertPromoCodeCampaign
|
||||
responses:
|
||||
200:
|
||||
description: "Operation completed!"
|
||||
/admin/promocodes/<id>:
|
||||
get:
|
||||
tags:
|
||||
- PromocodesApiV2
|
||||
summary: getPromoCodeCampaign
|
||||
description: "GET /api/v2/admin/promocodes/{id}\nReturns promocode campaign details (admin only)."
|
||||
operationId: getPromoCodeCampaign
|
||||
- PurchasesApiV2
|
||||
summary: getPackPurchaseStatus
|
||||
description: "GET /api/v2/purchases/packs/{packId}/status\nCheck if pack is purchased by the authenticated user"
|
||||
operationId: getPackPurchaseStatus
|
||||
parameters:
|
||||
- name: id
|
||||
- name: packId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
|
|
@ -56,14 +37,25 @@ paths:
|
|||
responses:
|
||||
200:
|
||||
description: "Operation completed!"
|
||||
delete:
|
||||
/purchases/payments:
|
||||
post:
|
||||
tags:
|
||||
- PromocodesApiV2
|
||||
summary: deletePromoCodeCampaign
|
||||
description: "DELETE /api/v2/admin/promocodes/{id}\nDeletes promocode campaign (admin only)."
|
||||
operationId: deletePromoCodeCampaign
|
||||
- PurchasesApiV2
|
||||
summary: createPayment
|
||||
description: POST /api/v2/purchases/payments\nCreate a payment\nCurrently supports YooKassa for web payments
|
||||
operationId: createPayment
|
||||
responses:
|
||||
200:
|
||||
description: "Operation completed!"
|
||||
/purchases/payments/<paymentId>/verify:
|
||||
get:
|
||||
tags:
|
||||
- PurchasesApiV2
|
||||
summary: verifyPayment
|
||||
description: "GET /api/v2/purchases/payments/{paymentId}/verify\nVerify payment status\nUpdates user purchases on success"
|
||||
operationId: verifyPayment
|
||||
parameters:
|
||||
- name: id
|
||||
- name: paymentId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
|
|
@ -71,104 +63,6 @@ paths:
|
|||
responses:
|
||||
200:
|
||||
description: "Operation completed!"
|
||||
/admin/discounts:
|
||||
get:
|
||||
tags:
|
||||
- DiscountsApiV2
|
||||
summary: GET /api/v2/admin/discounts
|
||||
operationId: listDiscountCampaigns
|
||||
responses:
|
||||
200:
|
||||
description: "Operation completed!"
|
||||
post:
|
||||
tags:
|
||||
- DiscountsApiV2
|
||||
summary: POST /api/v2/admin/discounts
|
||||
operationId: addDiscountCampaign
|
||||
responses:
|
||||
200:
|
||||
description: "Operation completed!"
|
||||
/admin/discounts/<id>:
|
||||
delete:
|
||||
tags:
|
||||
- DiscountsApiV2
|
||||
summary: "DELETE /api/v2/admin/discounts/{id}"
|
||||
operationId: deleteDiscountCampaign
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
200:
|
||||
description: "Operation completed!"
|
||||
/ads/product/acquire/<key>:
|
||||
post:
|
||||
tags:
|
||||
- AdsApiV2
|
||||
summary: "POST /api/v2/ads/product/acquire/{key}"
|
||||
description: Confirms rewarded ad completion and grants product access to the user.
|
||||
operationId: acquireProductForAd
|
||||
parameters:
|
||||
- name: key
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
200:
|
||||
description: "Operation completed!"
|
||||
/adsgram/reward:
|
||||
get:
|
||||
tags:
|
||||
- AdsApiV2
|
||||
summary: "GET /api/v2/adsgram/reward?userId={userId}"
|
||||
description: Callback endpoint for Adsgram rewarded ad completion.\nThis endpoint is called by Adsgram when a user completes a rewarded ad.
|
||||
operationId: adsgramRewardCallback
|
||||
responses:
|
||||
200:
|
||||
description: "Operation completed!"
|
||||
/subscriptions/plans:
|
||||
get:
|
||||
tags:
|
||||
- SubscriptionsApiV2
|
||||
summary: getPlans
|
||||
description: GET /api/v2/subscriptions/plans\nList available subscription plans
|
||||
operationId: getPlans
|
||||
responses:
|
||||
200:
|
||||
description: "Operation completed!"
|
||||
/subscriptions/purchase:
|
||||
post:
|
||||
tags:
|
||||
- SubscriptionsApiV2
|
||||
summary: purchase
|
||||
description: POST /api/v2/subscriptions/purchase\nPurchase a subscription
|
||||
operationId: purchase
|
||||
responses:
|
||||
200:
|
||||
description: "Operation completed!"
|
||||
/subscriptions/status:
|
||||
get:
|
||||
tags:
|
||||
- SubscriptionsApiV2
|
||||
summary: getStatus
|
||||
description: GET /api/v2/subscriptions/status\nGet current user subscription status
|
||||
operationId: getStatus
|
||||
responses:
|
||||
200:
|
||||
description: "Operation completed!"
|
||||
/subscriptions/cancel:
|
||||
post:
|
||||
tags:
|
||||
- SubscriptionsApiV2
|
||||
summary: cancel
|
||||
description: POST /api/v2/subscriptions/cancel\nCancel user’s subscription
|
||||
operationId: cancel
|
||||
responses:
|
||||
200:
|
||||
description: "Operation completed!"
|
||||
/users/me:
|
||||
get:
|
||||
tags:
|
||||
|
|
@ -339,185 +233,6 @@ paths:
|
|||
responses:
|
||||
200:
|
||||
description: "Operation completed!"
|
||||
/games:
|
||||
get:
|
||||
tags:
|
||||
- GamesApiV2
|
||||
summary: getGames
|
||||
description: GET /api/v2/games\nGet all available games\nReturns list of games with metadata
|
||||
operationId: getGames
|
||||
responses:
|
||||
200:
|
||||
description: "Operation completed!"
|
||||
/games/<gameId>/assets:
|
||||
get:
|
||||
tags:
|
||||
- GamesApiV2
|
||||
summary: getGameAssets
|
||||
description: "GET /api/v2/games/{gameId}/assets\nGet game assets\nReturns game assets file (zip) or asset info"
|
||||
operationId: getGameAssets
|
||||
parameters:
|
||||
- name: gameId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
200:
|
||||
description: "Operation completed!"
|
||||
/packs:
|
||||
get:
|
||||
tags:
|
||||
- PacksApiV2
|
||||
summary: getPacks
|
||||
description: "GET /api/v2/packs\nGet all pack previews with pagination\nQuery params: ?search=term&language=lang&page=1&limit=20"
|
||||
operationId: getPacks
|
||||
responses:
|
||||
200:
|
||||
description: "Operation completed!"
|
||||
/packs/<packId>:
|
||||
get:
|
||||
tags:
|
||||
- PacksApiV2
|
||||
summary: getPack
|
||||
description: "GET /api/v2/packs/{packId}\nGet pack details by ID\nReturns full pack details with purchase status if authenticated"
|
||||
operationId: getPack
|
||||
parameters:
|
||||
- name: packId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
200:
|
||||
description: "Operation completed!"
|
||||
/packs/<packId>/buy:
|
||||
get:
|
||||
tags:
|
||||
- PacksApiV2
|
||||
summary: getPackBuyPage
|
||||
description: "GET /api/v2/packs/{packId}/buy\nReturns pack purchase details (includes rewarded ads offer when available)"
|
||||
operationId: getPackBuyPage
|
||||
parameters:
|
||||
- name: packId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
200:
|
||||
description: "Operation completed!"
|
||||
/packs/<packId>/cards:
|
||||
get:
|
||||
tags:
|
||||
- PacksApiV2
|
||||
summary: getPackCards
|
||||
description: "GET /api/v2/packs/{packId}/cards\nGet all cards in a pack\nSupports pagination via query params: ?page=1&limit=20"
|
||||
operationId: getPackCards
|
||||
parameters:
|
||||
- name: packId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
200:
|
||||
description: "Operation completed!"
|
||||
/packs/<packId>/cards/<cardId>/image:
|
||||
get:
|
||||
tags:
|
||||
- PacksApiV2
|
||||
summary: getCardImage
|
||||
description: "GET /api/v2/packs/{packId}/cards/{cardId}/image\nGet card image\nReturns PNG image file\n\nImages are accessible for enabled packs even without authentication\nto allow image preview in public pack listings"
|
||||
operationId: getCardImage
|
||||
parameters:
|
||||
- name: packId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: cardId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
200:
|
||||
description: "Operation completed!"
|
||||
/packs/<packId>/tests:
|
||||
get:
|
||||
tags:
|
||||
- PacksApiV2
|
||||
summary: getPackTests
|
||||
description: "GET /api/v2/packs/{packId}/tests\nGet tests for a pack"
|
||||
operationId: getPackTests
|
||||
parameters:
|
||||
- name: packId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
200:
|
||||
description: "Operation completed!"
|
||||
/purchases/packs/<packId>:
|
||||
post:
|
||||
tags:
|
||||
- PurchasesApiV2
|
||||
summary: createPackPurchase
|
||||
description: "POST /api/v2/purchases/packs/{packId}\nCreate purchase intent for a pack\nReturns purchase info including payment URL for YooKassa"
|
||||
operationId: createPackPurchase
|
||||
parameters:
|
||||
- name: packId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
200:
|
||||
description: "Operation completed!"
|
||||
/purchases/packs/<packId>/status:
|
||||
get:
|
||||
tags:
|
||||
- PurchasesApiV2
|
||||
summary: getPackPurchaseStatus
|
||||
description: "GET /api/v2/purchases/packs/{packId}/status\nCheck if pack is purchased by the authenticated user"
|
||||
operationId: getPackPurchaseStatus
|
||||
parameters:
|
||||
- name: packId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
200:
|
||||
description: "Operation completed!"
|
||||
/purchases/payments:
|
||||
post:
|
||||
tags:
|
||||
- PurchasesApiV2
|
||||
summary: createPayment
|
||||
description: POST /api/v2/purchases/payments\nCreate a payment\nCurrently supports YooKassa for web payments
|
||||
operationId: createPayment
|
||||
responses:
|
||||
200:
|
||||
description: "Operation completed!"
|
||||
/purchases/payments/<paymentId>/verify:
|
||||
get:
|
||||
tags:
|
||||
- PurchasesApiV2
|
||||
summary: verifyPayment
|
||||
description: "GET /api/v2/purchases/payments/{paymentId}/verify\nVerify payment status\nUpdates user purchases on success"
|
||||
operationId: verifyPayment
|
||||
parameters:
|
||||
- name: paymentId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
200:
|
||||
description: "Operation completed!"
|
||||
/auth/oauth/google:
|
||||
post:
|
||||
tags:
|
||||
|
|
@ -662,6 +377,199 @@ paths:
|
|||
responses:
|
||||
200:
|
||||
description: "Operation completed!"
|
||||
/games:
|
||||
get:
|
||||
tags:
|
||||
- GamesApiV2
|
||||
summary: getGames
|
||||
description: GET /api/v2/games\nGet all available games\nReturns list of games with metadata
|
||||
operationId: getGames
|
||||
responses:
|
||||
200:
|
||||
description: "Operation completed!"
|
||||
/games/<gameId>/assets:
|
||||
get:
|
||||
tags:
|
||||
- GamesApiV2
|
||||
summary: getGameAssets
|
||||
description: "GET /api/v2/games/{gameId}/assets\nGet game assets\nReturns game assets file (zip) or asset info"
|
||||
operationId: getGameAssets
|
||||
parameters:
|
||||
- name: gameId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
200:
|
||||
description: "Operation completed!"
|
||||
/admin/discounts:
|
||||
get:
|
||||
tags:
|
||||
- DiscountsApiV2
|
||||
summary: GET /api/v2/admin/discounts
|
||||
operationId: listDiscountCampaigns
|
||||
responses:
|
||||
200:
|
||||
description: "Operation completed!"
|
||||
post:
|
||||
tags:
|
||||
- DiscountsApiV2
|
||||
summary: POST /api/v2/admin/discounts
|
||||
operationId: addDiscountCampaign
|
||||
responses:
|
||||
200:
|
||||
description: "Operation completed!"
|
||||
/admin/discounts/<id>:
|
||||
delete:
|
||||
tags:
|
||||
- DiscountsApiV2
|
||||
summary: "DELETE /api/v2/admin/discounts/{id}"
|
||||
operationId: deleteDiscountCampaign
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
200:
|
||||
description: "Operation completed!"
|
||||
/subscriptions/plans:
|
||||
get:
|
||||
tags:
|
||||
- SubscriptionsApiV2
|
||||
summary: getPlans
|
||||
description: GET /api/v2/subscriptions/plans\nList available subscription plans\nReturns all available subscription plans. Authentication is optional.
|
||||
operationId: getPlans
|
||||
responses:
|
||||
200:
|
||||
description: "Operation completed!"
|
||||
/subscriptions/purchase:
|
||||
post:
|
||||
tags:
|
||||
- SubscriptionsApiV2
|
||||
summary: purchase
|
||||
description: POST /api/v2/subscriptions/purchase\nPurchase a subscription
|
||||
operationId: purchase
|
||||
responses:
|
||||
200:
|
||||
description: "Operation completed!"
|
||||
/subscriptions/status:
|
||||
get:
|
||||
tags:
|
||||
- SubscriptionsApiV2
|
||||
summary: getStatus
|
||||
description: GET /api/v2/subscriptions/status\nGet current user subscription status
|
||||
operationId: getStatus
|
||||
responses:
|
||||
200:
|
||||
description: "Operation completed!"
|
||||
/subscriptions/cancel:
|
||||
post:
|
||||
tags:
|
||||
- SubscriptionsApiV2
|
||||
summary: cancel
|
||||
description: POST /api/v2/subscriptions/cancel\nCancel user’s subscription
|
||||
operationId: cancel
|
||||
responses:
|
||||
200:
|
||||
description: "Operation completed!"
|
||||
/packs:
|
||||
get:
|
||||
tags:
|
||||
- PacksApiV2
|
||||
summary: getPacks
|
||||
description: "GET /api/v2/packs\nGet all pack previews with pagination\nQuery params: ?search=term&language=lang&page=1&limit=20"
|
||||
operationId: getPacks
|
||||
responses:
|
||||
200:
|
||||
description: "Operation completed!"
|
||||
/packs/<packId>:
|
||||
get:
|
||||
tags:
|
||||
- PacksApiV2
|
||||
summary: getPack
|
||||
description: "GET /api/v2/packs/{packId}\nGet pack details by ID\nReturns full pack details with purchase status if authenticated"
|
||||
operationId: getPack
|
||||
parameters:
|
||||
- name: packId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
200:
|
||||
description: "Operation completed!"
|
||||
/packs/<packId>/buy:
|
||||
get:
|
||||
tags:
|
||||
- PacksApiV2
|
||||
summary: getPackBuyPage
|
||||
description: "GET /api/v2/packs/{packId}/buy\nReturns pack purchase details (includes rewarded ads offer when available)"
|
||||
operationId: getPackBuyPage
|
||||
parameters:
|
||||
- name: packId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
200:
|
||||
description: "Operation completed!"
|
||||
/packs/<packId>/cards:
|
||||
get:
|
||||
tags:
|
||||
- PacksApiV2
|
||||
summary: getPackCards
|
||||
description: "GET /api/v2/packs/{packId}/cards\nGet all cards in a pack\nSupports pagination via query params: ?page=1&limit=20"
|
||||
operationId: getPackCards
|
||||
parameters:
|
||||
- name: packId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
200:
|
||||
description: "Operation completed!"
|
||||
/packs/<packId>/cards/<cardId>/image:
|
||||
get:
|
||||
tags:
|
||||
- PacksApiV2
|
||||
summary: getCardImage
|
||||
description: "GET /api/v2/packs/{packId}/cards/{cardId}/image\nGet card image\nReturns PNG image file\n\nImages are accessible for enabled packs even without authentication\nto allow image preview in public pack listings"
|
||||
operationId: getCardImage
|
||||
parameters:
|
||||
- name: packId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: cardId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
200:
|
||||
description: "Operation completed!"
|
||||
/packs/<packId>/tests:
|
||||
get:
|
||||
tags:
|
||||
- PacksApiV2
|
||||
summary: getPackTests
|
||||
description: "GET /api/v2/packs/{packId}/tests\nGet tests for a pack"
|
||||
operationId: getPackTests
|
||||
parameters:
|
||||
- name: packId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
200:
|
||||
description: "Operation completed!"
|
||||
/tasks:
|
||||
get:
|
||||
tags:
|
||||
|
|
@ -734,29 +642,147 @@ paths:
|
|||
responses:
|
||||
200:
|
||||
description: "Operation completed!"
|
||||
/promocodes:
|
||||
get:
|
||||
tags:
|
||||
- PromocodesApiV2
|
||||
summary: listPromocodes
|
||||
description: GET /api/v2/promocodes\nLists available promocodes for current user.\nReturns active campaigns with promocodes that user can apply.
|
||||
operationId: listPromocodes
|
||||
responses:
|
||||
200:
|
||||
description: "Operation completed!"
|
||||
/promocodes/<code>/validate:
|
||||
get:
|
||||
tags:
|
||||
- PromocodesApiV2
|
||||
summary: validatePromocode
|
||||
description: "GET /api/v2/promocodes/{code}/validate\nValidates a promocode without applying it.\nReturns validation result with valid: true/false and message."
|
||||
operationId: validatePromocode
|
||||
parameters:
|
||||
- name: code
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
200:
|
||||
description: "Operation completed!"
|
||||
/promocodes/<code>/apply:
|
||||
post:
|
||||
tags:
|
||||
- PromocodesApiV2
|
||||
summary: applyPromocode
|
||||
description: "POST /api/v2/promocodes/{code}/apply\nApplies promocode for current user."
|
||||
operationId: applyPromocode
|
||||
parameters:
|
||||
- name: code
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
200:
|
||||
description: "Operation completed!"
|
||||
/admin/promocodes:
|
||||
get:
|
||||
tags:
|
||||
- PromocodesApiV2
|
||||
summary: listPromoCodeCampaigns
|
||||
description: GET /api/v2/admin/promocodes\nLists promocode campaigns (admin only).
|
||||
operationId: listPromoCodeCampaigns
|
||||
responses:
|
||||
200:
|
||||
description: "Operation completed!"
|
||||
post:
|
||||
tags:
|
||||
- PromocodesApiV2
|
||||
summary: upsertPromoCodeCampaign
|
||||
description: POST /api/v2/admin/promocodes\nCreates or updates promocode campaign (admin only).
|
||||
operationId: upsertPromoCodeCampaign
|
||||
responses:
|
||||
200:
|
||||
description: "Operation completed!"
|
||||
/admin/promocodes/<id>:
|
||||
get:
|
||||
tags:
|
||||
- PromocodesApiV2
|
||||
summary: getPromoCodeCampaign
|
||||
description: "GET /api/v2/admin/promocodes/{id}\nReturns promocode campaign details (admin only)."
|
||||
operationId: getPromoCodeCampaign
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
200:
|
||||
description: "Operation completed!"
|
||||
delete:
|
||||
tags:
|
||||
- PromocodesApiV2
|
||||
summary: deletePromoCodeCampaign
|
||||
description: "DELETE /api/v2/admin/promocodes/{id}\nDeletes promocode campaign (admin only)."
|
||||
operationId: deletePromoCodeCampaign
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
200:
|
||||
description: "Operation completed!"
|
||||
/ads/product/acquire/<key>:
|
||||
post:
|
||||
tags:
|
||||
- AdsApiV2
|
||||
summary: "POST /api/v2/ads/product/acquire/{key}"
|
||||
description: Confirms rewarded ad completion and grants product access to the user.
|
||||
operationId: acquireProductForAd
|
||||
parameters:
|
||||
- name: key
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
200:
|
||||
description: "Operation completed!"
|
||||
/adsgram/reward:
|
||||
get:
|
||||
tags:
|
||||
- AdsApiV2
|
||||
summary: "GET /api/v2/adsgram/reward?userId={userId}"
|
||||
description: Callback endpoint for Adsgram rewarded ad completion.\nThis endpoint is called by Adsgram when a user completes a rewarded ad.
|
||||
operationId: adsgramRewardCallback
|
||||
responses:
|
||||
200:
|
||||
description: "Operation completed!"
|
||||
components: { }
|
||||
tags:
|
||||
- name: PromocodesApiV2
|
||||
description: API v2 endpoints for promocode management and activation.
|
||||
- name: DiscountsApiV2
|
||||
description: Admin endpoints for discount campaign management.
|
||||
- name: AdsApiV2
|
||||
description: API v2 endpoints for rewarded ads flows.
|
||||
- name: SubscriptionsApiV2
|
||||
description: "Subscriptions API v2\nRESTful endpoints for managing subscriptions & plans"
|
||||
- name: PurchasesApiV2
|
||||
description: Purchases API v2\n\nRESTful endpoints for managing purchases and payments
|
||||
- name: UsersApiV2
|
||||
description: "API v2 endpoints for user profile and self-service operations."
|
||||
- name: AdminUsersApiV2
|
||||
description: Admin endpoints for user management in API v2.
|
||||
- name: GamesApiV2
|
||||
description: Games API v2\n\nRESTful endpoints for managing games and game assets
|
||||
- name: PacksApiV2
|
||||
description: API v2 Packs endpoints\n\nRESTful endpoints for card packs with pagination and filtering
|
||||
- name: PurchasesApiV2
|
||||
description: Purchases API v2\n\nRESTful endpoints for managing purchases and payments
|
||||
- name: AuthApiV2
|
||||
description: API v2 Authentication endpoints\n\nImplements OAuth2/JWT Bearer token authentication
|
||||
- name: TestsApiV2
|
||||
description: Tests API v2\n\nRESTful endpoints for managing tests and test results
|
||||
- name: GamesApiV2
|
||||
description: Games API v2\n\nRESTful endpoints for managing games and game assets
|
||||
- name: DiscountsApiV2
|
||||
description: Admin endpoints for discount campaign management.
|
||||
- name: SubscriptionsApiV2
|
||||
description: "Subscriptions API v2\nRESTful endpoints for managing subscriptions & plans"
|
||||
- name: PacksApiV2
|
||||
description: API v2 Packs endpoints\n\nRESTful endpoints for card packs with pagination and filtering
|
||||
- name: TasksApiV2
|
||||
description: API v2 endpoints for user tasks management
|
||||
- name: PromocodesApiV2
|
||||
description: API v2 endpoints for promocode management and activation.
|
||||
- name: AdsApiV2
|
||||
description: API v2 endpoints for rewarded ads flows.
|
||||
|
|
@ -550,4 +550,539 @@ void main() {
|
|||
expect(campaign['promoCodes'], isA<List>());
|
||||
});
|
||||
});
|
||||
|
||||
group('GET /api/v2/promocodes/{code}/validate', () {
|
||||
test('returns 401 for unauthenticated requests', () async {
|
||||
final request = buildRequest('GET', '/api/v2/promocodes/TEST123/validate');
|
||||
final response = await promocodesApiV2.validatePromocode(request, 'TEST123');
|
||||
|
||||
expect(response.statusCode, equals(401));
|
||||
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
expect(body['error'], equals('unauthorized'));
|
||||
});
|
||||
|
||||
test('returns 404 for non-existent promocode', () async {
|
||||
final request = buildRequest(
|
||||
'GET',
|
||||
'/api/v2/promocodes/NONEXISTENT/validate',
|
||||
user: testUser,
|
||||
);
|
||||
final response = await promocodesApiV2.validatePromocode(request, 'NONEXISTENT');
|
||||
|
||||
expect(response.statusCode, equals(404));
|
||||
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
expect(body['valid'], equals(false));
|
||||
expect(body['message'], equals('Промокод не найден'));
|
||||
});
|
||||
|
||||
test('returns 400 for empty promocode', () async {
|
||||
final request = buildRequest(
|
||||
'GET',
|
||||
'/api/v2/promocodes//validate',
|
||||
user: testUser,
|
||||
);
|
||||
final response = await promocodesApiV2.validatePromocode(request, '');
|
||||
|
||||
expect(response.statusCode, equals(400));
|
||||
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
expect(body['error'], equals('bad_request'));
|
||||
});
|
||||
|
||||
test('returns valid: true for valid promocode', () 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: 'VALID',
|
||||
products: [],
|
||||
activationsPerCode: 10,
|
||||
activationsPerUser: 1,
|
||||
generationSize: 100,
|
||||
start: start,
|
||||
finish: finish,
|
||||
status: PromoCodeCampaignModelStatus.active,
|
||||
tags: [],
|
||||
);
|
||||
await testIsar.promoCodesCampaignModels.put(campaign);
|
||||
|
||||
final promoCode = PromoCodeModel(code: 'VALID123', activations: 0);
|
||||
await testIsar.promoCodeModels.put(promoCode);
|
||||
campaign.promoCodes.add(promoCode);
|
||||
await campaign.promoCodes.save();
|
||||
});
|
||||
|
||||
final request = buildRequest(
|
||||
'GET',
|
||||
'/api/v2/promocodes/VALID123/validate',
|
||||
user: testUser,
|
||||
);
|
||||
final response = await promocodesApiV2.validatePromocode(request, 'VALID123');
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
expect(body['valid'], equals(true));
|
||||
expect(body['message'], equals('Промокод действителен'));
|
||||
});
|
||||
|
||||
test('returns valid: false for inactive campaign', () 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: 'INACTIVE',
|
||||
products: [],
|
||||
activationsPerCode: 10,
|
||||
activationsPerUser: 1,
|
||||
generationSize: 100,
|
||||
start: start,
|
||||
finish: finish,
|
||||
status: PromoCodeCampaignModelStatus.disabled,
|
||||
tags: [],
|
||||
);
|
||||
await testIsar.promoCodesCampaignModels.put(campaign);
|
||||
|
||||
final promoCode = PromoCodeModel(code: 'INACTIVE123', activations: 0);
|
||||
await testIsar.promoCodeModels.put(promoCode);
|
||||
campaign.promoCodes.add(promoCode);
|
||||
await campaign.promoCodes.save();
|
||||
});
|
||||
|
||||
final request = buildRequest(
|
||||
'GET',
|
||||
'/api/v2/promocodes/INACTIVE123/validate',
|
||||
user: testUser,
|
||||
);
|
||||
final response = await promocodesApiV2.validatePromocode(request, 'INACTIVE123');
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
expect(body['valid'], equals(false));
|
||||
expect(body['message'], equals('Промокод недействителен'));
|
||||
});
|
||||
|
||||
test('returns valid: false for promocode that has not started', () async {
|
||||
final now = DateTime.now();
|
||||
final start = now.add(const Duration(days: 1));
|
||||
final finish = now.add(const Duration(days: 2));
|
||||
|
||||
await testIsar.writeTxn(() async {
|
||||
final campaign = PromoCodesCampaignModel(
|
||||
template: 'FUTURE',
|
||||
products: [],
|
||||
activationsPerCode: 10,
|
||||
activationsPerUser: 1,
|
||||
generationSize: 100,
|
||||
start: start,
|
||||
finish: finish,
|
||||
status: PromoCodeCampaignModelStatus.active,
|
||||
tags: [],
|
||||
);
|
||||
await testIsar.promoCodesCampaignModels.put(campaign);
|
||||
|
||||
final promoCode = PromoCodeModel(code: 'FUTURE123', activations: 0);
|
||||
await testIsar.promoCodeModels.put(promoCode);
|
||||
campaign.promoCodes.add(promoCode);
|
||||
await campaign.promoCodes.save();
|
||||
});
|
||||
|
||||
final request = buildRequest(
|
||||
'GET',
|
||||
'/api/v2/promocodes/FUTURE123/validate',
|
||||
user: testUser,
|
||||
);
|
||||
final response = await promocodesApiV2.validatePromocode(request, 'FUTURE123');
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
expect(body['valid'], equals(false));
|
||||
expect(body['message'], equals('Промокод еще не активен'));
|
||||
});
|
||||
|
||||
test('returns valid: false for expired promocode', () async {
|
||||
final now = DateTime.now();
|
||||
final start = now.subtract(const Duration(days: 2));
|
||||
final finish = now.subtract(const Duration(days: 1));
|
||||
|
||||
await testIsar.writeTxn(() async {
|
||||
final campaign = PromoCodesCampaignModel(
|
||||
template: 'EXPIRED',
|
||||
products: [],
|
||||
activationsPerCode: 10,
|
||||
activationsPerUser: 1,
|
||||
generationSize: 100,
|
||||
start: start,
|
||||
finish: finish,
|
||||
status: PromoCodeCampaignModelStatus.active,
|
||||
tags: [],
|
||||
);
|
||||
await testIsar.promoCodesCampaignModels.put(campaign);
|
||||
|
||||
final promoCode = PromoCodeModel(code: 'EXPIRED123', activations: 0);
|
||||
await testIsar.promoCodeModels.put(promoCode);
|
||||
campaign.promoCodes.add(promoCode);
|
||||
await campaign.promoCodes.save();
|
||||
});
|
||||
|
||||
final request = buildRequest(
|
||||
'GET',
|
||||
'/api/v2/promocodes/EXPIRED123/validate',
|
||||
user: testUser,
|
||||
);
|
||||
final response = await promocodesApiV2.validatePromocode(request, 'EXPIRED123');
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
expect(body['valid'], equals(false));
|
||||
expect(body['message'], equals('Промокод истек'));
|
||||
});
|
||||
|
||||
test('returns valid: false for promocode 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: 'EXHAUSTED',
|
||||
products: [],
|
||||
activationsPerCode: 1,
|
||||
activationsPerUser: 10,
|
||||
generationSize: 100,
|
||||
start: start,
|
||||
finish: finish,
|
||||
status: PromoCodeCampaignModelStatus.active,
|
||||
tags: [],
|
||||
);
|
||||
await testIsar.promoCodesCampaignModels.put(campaign);
|
||||
|
||||
final promoCode = PromoCodeModel(code: 'EXHAUSTED123', activations: 1);
|
||||
await testIsar.promoCodeModels.put(promoCode);
|
||||
campaign.promoCodes.add(promoCode);
|
||||
await campaign.promoCodes.save();
|
||||
});
|
||||
|
||||
final request = buildRequest(
|
||||
'GET',
|
||||
'/api/v2/promocodes/EXHAUSTED123/validate',
|
||||
user: testUser,
|
||||
);
|
||||
final response = await promocodesApiV2.validatePromocode(request, 'EXHAUSTED123');
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
expect(body['valid'], equals(false));
|
||||
expect(body['message'], equals('Промокод исчерпан'));
|
||||
});
|
||||
|
||||
test('returns valid: false for promocode 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: 'USED',
|
||||
products: [],
|
||||
activationsPerCode: 10,
|
||||
activationsPerUser: 10,
|
||||
generationSize: 100,
|
||||
start: start,
|
||||
finish: finish,
|
||||
status: PromoCodeCampaignModelStatus.active,
|
||||
tags: [],
|
||||
);
|
||||
await testIsar.promoCodesCampaignModels.put(campaign);
|
||||
|
||||
final promoCode = PromoCodeModel(code: 'USED123', activations: 0);
|
||||
await testIsar.promoCodeModels.put(promoCode);
|
||||
campaign.promoCodes.add(promoCode);
|
||||
await campaign.promoCodes.save();
|
||||
|
||||
// User has already activated this code
|
||||
testUserData.activatedPromoCodes.add(promoCode);
|
||||
await testUserData.activatedPromoCodes.save();
|
||||
await testIsar.userDataModels.put(testUserData);
|
||||
});
|
||||
|
||||
final request = buildRequest(
|
||||
'GET',
|
||||
'/api/v2/promocodes/USED123/validate',
|
||||
user: testUser,
|
||||
);
|
||||
final response = await promocodesApiV2.validatePromocode(request, 'USED123');
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
expect(body['valid'], equals(false));
|
||||
expect(body['message'], equals('Промокод уже был активирован'));
|
||||
});
|
||||
|
||||
test('returns valid: false when user reached campaign 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 promoCode1 = PromoCodeModel(code: 'LIMITED1', activations: 0);
|
||||
final promoCode2 = PromoCodeModel(code: 'LIMITED2', activations: 0);
|
||||
await testIsar.promoCodeModels.putAll([promoCode1, promoCode2]);
|
||||
campaign.promoCodes.addAll([promoCode1, promoCode2]);
|
||||
await campaign.promoCodes.save();
|
||||
|
||||
// User has already activated one code from this campaign
|
||||
testUserData.activatedPromoCodes.add(promoCode1);
|
||||
await testUserData.activatedPromoCodes.save();
|
||||
await testIsar.userDataModels.put(testUserData);
|
||||
});
|
||||
|
||||
final request = buildRequest(
|
||||
'GET',
|
||||
'/api/v2/promocodes/LIMITED2/validate',
|
||||
user: testUser,
|
||||
);
|
||||
final response = await promocodesApiV2.validatePromocode(request, 'LIMITED2');
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
expect(body['valid'], equals(false));
|
||||
expect(body['message'], equals('Вы уже участвовали в этой акции'));
|
||||
});
|
||||
|
||||
test('returns valid: false when user tags do not match campaign 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 {
|
||||
final campaign = PromoCodesCampaignModel(
|
||||
template: 'TAGGED',
|
||||
products: [],
|
||||
activationsPerCode: 10,
|
||||
activationsPerUser: 1,
|
||||
generationSize: 100,
|
||||
start: start,
|
||||
finish: finish,
|
||||
status: PromoCodeCampaignModelStatus.active,
|
||||
tags: ['vip'], // User has ['premium', 'beta'], no 'vip'
|
||||
);
|
||||
await testIsar.promoCodesCampaignModels.put(campaign);
|
||||
|
||||
final promoCode = PromoCodeModel(code: 'TAGGED123', activations: 0);
|
||||
await testIsar.promoCodeModels.put(promoCode);
|
||||
campaign.promoCodes.add(promoCode);
|
||||
await campaign.promoCodes.save();
|
||||
});
|
||||
|
||||
final request = buildRequest(
|
||||
'GET',
|
||||
'/api/v2/promocodes/TAGGED123/validate',
|
||||
user: testUser,
|
||||
);
|
||||
final response = await promocodesApiV2.validatePromocode(request, 'TAGGED123');
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
expect(body['valid'], equals(false));
|
||||
expect(body['message'], equals('Промокод недействителен'));
|
||||
});
|
||||
|
||||
test('returns valid: true when user tags match campaign 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 {
|
||||
final campaign = PromoCodesCampaignModel(
|
||||
template: 'MATCHING',
|
||||
products: [],
|
||||
activationsPerCode: 10,
|
||||
activationsPerUser: 1,
|
||||
generationSize: 100,
|
||||
start: start,
|
||||
finish: finish,
|
||||
status: PromoCodeCampaignModelStatus.active,
|
||||
tags: ['premium'], // User has ['premium', 'beta']
|
||||
);
|
||||
await testIsar.promoCodesCampaignModels.put(campaign);
|
||||
|
||||
final promoCode = PromoCodeModel(code: 'MATCHING123', activations: 0);
|
||||
await testIsar.promoCodeModels.put(promoCode);
|
||||
campaign.promoCodes.add(promoCode);
|
||||
await campaign.promoCodes.save();
|
||||
});
|
||||
|
||||
final request = buildRequest(
|
||||
'GET',
|
||||
'/api/v2/promocodes/MATCHING123/validate',
|
||||
user: testUser,
|
||||
);
|
||||
final response = await promocodesApiV2.validatePromocode(request, 'MATCHING123');
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
expect(body['valid'], equals(true));
|
||||
expect(body['message'], equals('Промокод действителен'));
|
||||
});
|
||||
|
||||
test('returns valid: false for individual promocode for another user', () async {
|
||||
final now = DateTime.now();
|
||||
final start = now.subtract(const Duration(days: 1));
|
||||
final finish = now.add(const Duration(days: 1));
|
||||
|
||||
late UserModel otherUser;
|
||||
late UserDataModel otherUserData;
|
||||
|
||||
await testIsar.writeTxn(() async {
|
||||
// Create another user
|
||||
otherUser = UserModel.empty.copyWith(
|
||||
id: 2,
|
||||
name: 'Other User',
|
||||
email: 'other@example.com',
|
||||
);
|
||||
await testIsar.userModels.put(otherUser);
|
||||
|
||||
otherUserData = UserDataModel(
|
||||
tags: [],
|
||||
words: [],
|
||||
);
|
||||
await testIsar.userDataModels.put(otherUserData);
|
||||
otherUserData.user.value = otherUser;
|
||||
await otherUserData.user.save();
|
||||
otherUser.userData.value = otherUserData;
|
||||
await otherUser.userData.save();
|
||||
|
||||
final campaign = PromoCodesCampaignModel(
|
||||
template: 'INDIVIDUAL',
|
||||
products: [],
|
||||
activationsPerCode: 10,
|
||||
activationsPerUser: 1,
|
||||
generationSize: 100,
|
||||
start: start,
|
||||
finish: finish,
|
||||
status: PromoCodeCampaignModelStatus.active,
|
||||
tags: [],
|
||||
);
|
||||
await testIsar.promoCodesCampaignModels.put(campaign);
|
||||
|
||||
final promoCode = PromoCodeModel(code: 'INDIVIDUAL123', activations: 0);
|
||||
await testIsar.promoCodeModels.put(promoCode);
|
||||
campaign.promoCodes.add(promoCode);
|
||||
await campaign.promoCodes.save();
|
||||
|
||||
// Assign promocode to other user
|
||||
promoCode.userData.value = otherUserData;
|
||||
await promoCode.userData.save();
|
||||
});
|
||||
|
||||
final request = buildRequest(
|
||||
'GET',
|
||||
'/api/v2/promocodes/INDIVIDUAL123/validate',
|
||||
user: testUser,
|
||||
);
|
||||
final response = await promocodesApiV2.validatePromocode(request, 'INDIVIDUAL123');
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
expect(body['valid'], equals(false));
|
||||
expect(body['message'], equals('Это промокод для другого пользователя'));
|
||||
});
|
||||
|
||||
test('returns valid: true for individual promocode for current 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: 'MYCODE',
|
||||
products: [],
|
||||
activationsPerCode: 10,
|
||||
activationsPerUser: 1,
|
||||
generationSize: 100,
|
||||
start: start,
|
||||
finish: finish,
|
||||
status: PromoCodeCampaignModelStatus.active,
|
||||
tags: [],
|
||||
);
|
||||
await testIsar.promoCodesCampaignModels.put(campaign);
|
||||
|
||||
final promoCode = PromoCodeModel(code: 'MYCODE123', activations: 0);
|
||||
await testIsar.promoCodeModels.put(promoCode);
|
||||
campaign.promoCodes.add(promoCode);
|
||||
await campaign.promoCodes.save();
|
||||
|
||||
// Assign promocode to current user
|
||||
promoCode.userData.value = testUserData;
|
||||
await promoCode.userData.save();
|
||||
});
|
||||
|
||||
final request = buildRequest(
|
||||
'GET',
|
||||
'/api/v2/promocodes/MYCODE123/validate',
|
||||
user: testUser,
|
||||
);
|
||||
final response = await promocodesApiV2.validatePromocode(request, 'MYCODE123');
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
expect(body['valid'], equals(true));
|
||||
expect(body['message'], equals('Промокод действителен'));
|
||||
});
|
||||
|
||||
test('handles case-insensitive promocode', () 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: 'CASE',
|
||||
products: [],
|
||||
activationsPerCode: 10,
|
||||
activationsPerUser: 1,
|
||||
generationSize: 100,
|
||||
start: start,
|
||||
finish: finish,
|
||||
status: PromoCodeCampaignModelStatus.active,
|
||||
tags: [],
|
||||
);
|
||||
await testIsar.promoCodesCampaignModels.put(campaign);
|
||||
|
||||
final promoCode = PromoCodeModel(code: 'CASE123', activations: 0);
|
||||
await testIsar.promoCodeModels.put(promoCode);
|
||||
campaign.promoCodes.add(promoCode);
|
||||
await campaign.promoCodes.save();
|
||||
});
|
||||
|
||||
// Request with lowercase code
|
||||
final request = buildRequest(
|
||||
'GET',
|
||||
'/api/v2/promocodes/case123/validate',
|
||||
user: testUser,
|
||||
);
|
||||
final response = await promocodesApiV2.validatePromocode(request, 'case123');
|
||||
|
||||
expect(response.statusCode, equals(200));
|
||||
final body = jsonDecode(await response.readAsString()) as Map<String, dynamic>;
|
||||
expect(body['valid'], equals(true));
|
||||
expect(body['message'], equals('Промокод действителен'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue