From 288b85a59b29a8ec41b34f648bb7d10127153259 Mon Sep 17 00:00:00 2001 From: masoodafar-web Date: Tue, 23 Dec 2025 22:52:49 +0330 Subject: [PATCH] feat: implement Chatika account activation job for new club members --- docs/chatika.postman_collection.json | 421 ++++++++++++++++++ ...eptClubMembershipContractCommandHandler.cs | 3 +- .../ActivateClubMembershipCommandHandler.cs | 3 +- .../Common/Interfaces/IChatikaApiService.cs | 64 +++ .../Enums/ClubFeatureType.cs | 58 +++ .../ChatikaAccountActivationJob.cs | 183 ++++++++ .../ConfigureServices.cs | 10 + .../Services/ChatikaApiService.cs | 156 +++++++ src/CMSMicroservice.WebApi/Program.cs | 8 + src/CMSMicroservice.WebApi/appsettings.json | 4 + 10 files changed, 908 insertions(+), 2 deletions(-) create mode 100644 docs/chatika.postman_collection.json create mode 100644 src/CMSMicroservice.Application/Common/Interfaces/IChatikaApiService.cs create mode 100644 src/CMSMicroservice.Domain/Enums/ClubFeatureType.cs create mode 100644 src/CMSMicroservice.Infrastructure/BackgroundJobs/ChatikaAccountActivationJob.cs create mode 100644 src/CMSMicroservice.Infrastructure/Services/ChatikaApiService.cs diff --git a/docs/chatika.postman_collection.json b/docs/chatika.postman_collection.json new file mode 100644 index 0000000..ed27429 --- /dev/null +++ b/docs/chatika.postman_collection.json @@ -0,0 +1,421 @@ +{ + "info": { + "_postman_id": "a5404a90-7d5a-4e9f-a26b-bb2bc93f19a2", + "name": "hushyar", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "12336342", + "_collection_link": "https://abbas-rahimzadeh-postman.postman.co/workspace/public-workspace~57c702f9-fed1-4b27-b507-0b0252812bf7/collection/12336342-a5404a90-7d5a-4e9f-a26b-bb2bc93f19a2?action=share&source=collection_link&creator=12336342" + }, + "item": [ + { + "name": "v1", + "item": [ + { + "name": "organization", + "item": [ + { + "name": "register-user", + "request": { + "method": "POST", + "header": [ + { + "key": "X-API-Key", + "value": "{{organizationApiKey}}", + "description": "Organization API Key", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"mobile_number\": \"09123456789\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseURL}}/api/v1/organizations/register-user", + "host": [ + "{{baseURL}}" + ], + "path": [ + "api", + "v1", + "organizations", + "register-user" + ] + }, + "description": "Register a new user or get existing user via organization API key. If user is new or has no organization, they will be assigned to this organization and their wallet will be charged with the organization's user_credit." + }, + "response": [ + { + "name": "success - new user", + "originalRequest": { + "method": "POST", + "header": [ + { + "key": "X-API-Key", + "value": "{{organizationApiKey}}", + "description": "Organization API Key", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"mobile_number\": \"09123456789\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseURL}}/api/v1/organizations/register-user", + "host": [ + "{{baseURL}}" + ], + "path": [ + "api", + "v1", + "organizations", + "register-user" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "content-type", + "value": "application/json" + } + ], + "cookie": [], + "body": "{\n \"id\": 1,\n \"mobile_number\": \"09123456789\",\n \"organization_id\": 1,\n \"organization_title\": \"Test Organization\",\n \"wallet_balance\": 100.0,\n \"is_new_user\": true,\n \"credit_charged\": 100.0\n}" + }, + { + "name": "success - existing user", + "originalRequest": { + "method": "POST", + "header": [ + { + "key": "X-API-Key", + "value": "{{organizationApiKey}}", + "description": "Organization API Key", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"mobile_number\": \"09123456789\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseURL}}/api/v1/organizations/register-user", + "host": [ + "{{baseURL}}" + ], + "path": [ + "api", + "v1", + "organizations", + "register-user" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "content-type", + "value": "application/json" + } + ], + "cookie": [], + "body": "{\n \"id\": 1,\n \"mobile_number\": \"09123456789\",\n \"organization_id\": 1,\n \"organization_title\": \"Test Organization\",\n \"wallet_balance\": 100.0,\n \"is_new_user\": false,\n \"credit_charged\": 0.0\n}" + }, + { + "name": "invalid api key", + "originalRequest": { + "method": "POST", + "header": [ + { + "key": "X-API-Key", + "value": "invalid-api-key", + "description": "Organization API Key", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"mobile_number\": \"09123456789\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseURL}}/api/v1/organizations/register-user", + "host": [ + "{{baseURL}}" + ], + "path": [ + "api", + "v1", + "organizations", + "register-user" + ] + } + }, + "status": "Unauthorized", + "code": 401, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "content-type", + "value": "application/json" + } + ], + "cookie": [], + "body": "{\n \"error_code\": \"INVALID_API_KEY\",\n \"message\": \"Invalid API key\",\n \"context\": {\n \"api_key_prefix\": \"invalid-\"\n }\n}" + }, + { + "name": "organization disabled", + "originalRequest": { + "method": "POST", + "header": [ + { + "key": "X-API-Key", + "value": "{{organizationApiKey}}", + "description": "Organization API Key", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"mobile_number\": \"09123456789\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseURL}}/api/v1/organizations/register-user", + "host": [ + "{{baseURL}}" + ], + "path": [ + "api", + "v1", + "organizations", + "register-user" + ] + } + }, + "status": "Forbidden", + "code": 403, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "content-type", + "value": "application/json" + } + ], + "cookie": [], + "body": "{\n \"error_code\": \"ORGANIZATION_DISABLED\",\n \"message\": \"Organization is disabled\",\n \"context\": {\n \"organization_id\": 1\n }\n}" + }, + { + "name": "organization expired", + "originalRequest": { + "method": "POST", + "header": [ + { + "key": "X-API-Key", + "value": "{{organizationApiKey}}", + "description": "Organization API Key", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"mobile_number\": \"09123456789\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseURL}}/api/v1/organizations/register-user", + "host": [ + "{{baseURL}}" + ], + "path": [ + "api", + "v1", + "organizations", + "register-user" + ] + } + }, + "status": "Forbidden", + "code": 403, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "content-type", + "value": "application/json" + } + ], + "cookie": [], + "body": "{\n \"error_code\": \"ORGANIZATION_EXPIRED\",\n \"message\": \"Organization has expired\",\n \"context\": {\n \"organization_id\": 1,\n \"expires_at\": \"2024-01-01T00:00:00\"\n }\n}" + }, + { + "name": "invalid mobile format", + "originalRequest": { + "method": "POST", + "header": [ + { + "key": "X-API-Key", + "value": "{{organizationApiKey}}", + "description": "Organization API Key", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"mobile_number\": \"01234567890\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseURL}}/api/v1/organizations/register-user", + "host": [ + "{{baseURL}}" + ], + "path": [ + "api", + "v1", + "organizations", + "register-user" + ] + } + }, + "status": "Unprocessable Entity", + "code": 422, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "content-type", + "value": "application/json" + } + ], + "cookie": [], + "body": "{\n \"mobile_number\": \"String should match pattern '^09\\\\d{9}$'\"\n}" + }, + { + "name": "missing api key header", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"mobile_number\": \"09123456789\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseURL}}/api/v1/organizations/register-user", + "host": [ + "{{baseURL}}" + ], + "path": [ + "api", + "v1", + "organizations", + "register-user" + ] + } + }, + "status": "Unprocessable Entity", + "code": 422, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "content-type", + "value": "application/json" + } + ], + "cookie": [], + "body": "{\n \"detail\": [\n {\n \"type\": \"missing\",\n \"loc\": [\"header\", \"x-api-key\"],\n \"msg\": \"Field required\",\n \"input\": null\n }\n ]\n}" + } + ] + } + ] + } + ] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "packages": {}, + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "packages": {}, + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "baseURL", + "value": "http://127.0.0.0:8000", + "type": "string" + }, + { + "key": "access_token", + "value": "", + "type": "string" + }, + { + "key": "accessToken", + "value": "" + }, + { + "key": "refreshToken", + "value": "" + }, + { + "key": "organizationApiKey", + "value": "", + "type": "string" + } + ] +} \ No newline at end of file diff --git a/src/CMSMicroservice.Application/ClubMembershipCQ/Commands/AcceptClubMembershipContract/AcceptClubMembershipContractCommandHandler.cs b/src/CMSMicroservice.Application/ClubMembershipCQ/Commands/AcceptClubMembershipContract/AcceptClubMembershipContractCommandHandler.cs index 9004fcb..2067b07 100644 --- a/src/CMSMicroservice.Application/ClubMembershipCQ/Commands/AcceptClubMembershipContract/AcceptClubMembershipContractCommandHandler.cs +++ b/src/CMSMicroservice.Application/ClubMembershipCQ/Commands/AcceptClubMembershipContract/AcceptClubMembershipContractCommandHandler.cs @@ -261,8 +261,9 @@ public class AcceptClubMembershipContractCommandHandler // 10. اعطای ویژگی‌های باشگاه برای کاربر (فقط برای عضویت جدید) if (isNewMembership) { + var featureIds = ClubFeatureTypeExtensions.GetAllFeatureIds(); var clubFeatures = await _context.ClubFeatures - .Where(f => !f.IsDeleted && new long[] { 1, 2, 3, 4 }.Contains(f.Id)) + .Where(f => !f.IsDeleted && featureIds.Contains(f.Id)) .ToListAsync(cancellationToken); if (clubFeatures.Any()) diff --git a/src/CMSMicroservice.Application/ClubMembershipCQ/Commands/ActivateClubMembership/ActivateClubMembershipCommandHandler.cs b/src/CMSMicroservice.Application/ClubMembershipCQ/Commands/ActivateClubMembership/ActivateClubMembershipCommandHandler.cs index 109e4f7..57fef67 100644 --- a/src/CMSMicroservice.Application/ClubMembershipCQ/Commands/ActivateClubMembership/ActivateClubMembershipCommandHandler.cs +++ b/src/CMSMicroservice.Application/ClubMembershipCQ/Commands/ActivateClubMembership/ActivateClubMembershipCommandHandler.cs @@ -297,8 +297,9 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler !f.IsDeleted && new long[] { 1, 2, 3, 4 }.Contains(f.Id)) + .Where(f => !f.IsDeleted && featureIds.Contains(f.Id)) .ToListAsync(cancellationToken); if (clubFeatures.Any()) diff --git a/src/CMSMicroservice.Application/Common/Interfaces/IChatikaApiService.cs b/src/CMSMicroservice.Application/Common/Interfaces/IChatikaApiService.cs new file mode 100644 index 0000000..6a05b62 --- /dev/null +++ b/src/CMSMicroservice.Application/Common/Interfaces/IChatikaApiService.cs @@ -0,0 +1,64 @@ +namespace CMSMicroservice.Application.Common.Interfaces; + +/// +/// سرویس ارتباط با API چتیکا برای ساخت حساب کاربری +/// +public interface IChatikaApiService +{ + /// + /// ایجاد حساب کاربری در چتیکا + /// + /// شماره موبایل کاربر + /// نام کامل کاربر + /// Cancellation token + /// نتیجه ایجاد حساب + Task CreateAccountAsync( + string mobileNumber, + string fullName, + CancellationToken cancellationToken = default); +} + +/// +/// نتیجه ایجاد حساب در چتیکا +/// +public class ChatikaAccountResult +{ + /// + /// موفقیت عملیات + /// + public bool IsSuccess { get; set; } + + /// + /// پیام خطا در صورت عدم موفقیت + /// + public string? ErrorMessage { get; set; } + + /// + /// شناسه کاربر در چتیکا + /// + public string? ChatikaUserId { get; set; } + + /// + /// URL دسترسی به چتیکا + /// + public string? AccessUrl { get; set; } + + /// + /// ایجاد نتیجه موفق + /// + public static ChatikaAccountResult Success(string? chatikaUserId = null, string? accessUrl = null) => new() + { + IsSuccess = true, + ChatikaUserId = chatikaUserId, + AccessUrl = accessUrl + }; + + /// + /// ایجاد نتیجه ناموفق + /// + public static ChatikaAccountResult Failure(string errorMessage) => new() + { + IsSuccess = false, + ErrorMessage = errorMessage + }; +} diff --git a/src/CMSMicroservice.Domain/Enums/ClubFeatureType.cs b/src/CMSMicroservice.Domain/Enums/ClubFeatureType.cs new file mode 100644 index 0000000..977eb97 --- /dev/null +++ b/src/CMSMicroservice.Domain/Enums/ClubFeatureType.cs @@ -0,0 +1,58 @@ +namespace CMSMicroservice.Domain.Enums; + +/// +/// انواع ویژگی‌های باشگاه مشتریان +/// +public enum ClubFeatureType +{ + /// + /// چتیکا - دستیار هوش مصنوعی + /// + Chatika = 1, + + /// + /// بیمه - خدمات بیمه‌ای + /// + Bime = 2, + + /// + /// تریپ - خدمات سفر و گردشگری + /// + Trip = 3, + + /// + /// لرن - آموزش و یادگیری + /// + Learn = 4 +} + +/// +/// Extension methods برای ClubFeatureType +/// +public static class ClubFeatureTypeExtensions +{ + /// + /// دریافت تمام مقادیر ClubFeatureType به صورت آرایه long + /// + public static long[] GetAllFeatureIds() + { + return Enum.GetValues() + .Select(f => (long)f) + .ToArray(); + } + + /// + /// دریافت عنوان فارسی ویژگی + /// + public static string GetPersianTitle(this ClubFeatureType featureType) + { + return featureType switch + { + ClubFeatureType.Chatika => "چتیکا", + ClubFeatureType.Bime => "بیمه", + ClubFeatureType.Trip => "تور و سفر", + ClubFeatureType.Learn => "آموزش", + _ => featureType.ToString() + }; + } +} diff --git a/src/CMSMicroservice.Infrastructure/BackgroundJobs/ChatikaAccountActivationJob.cs b/src/CMSMicroservice.Infrastructure/BackgroundJobs/ChatikaAccountActivationJob.cs new file mode 100644 index 0000000..663f24f --- /dev/null +++ b/src/CMSMicroservice.Infrastructure/BackgroundJobs/ChatikaAccountActivationJob.cs @@ -0,0 +1,183 @@ +using CMSMicroservice.Application.Common.Interfaces; +using CMSMicroservice.Domain.Entities.Club; +using CMSMicroservice.Domain.Enums; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Polly; + +namespace CMSMicroservice.Infrastructure.BackgroundJobs; + +/// +/// Hangfire Job برای فعال‌سازی حساب چتیکا برای اعضای جدید باشگاه +/// این Job کاربرانی که باشگاهشان فعال شده ولی حساب چتیکا ندارند را پیدا کرده و حساب می‌سازد +/// +public class ChatikaAccountActivationJob +{ + private readonly IApplicationDbContext _context; + private readonly IChatikaApiService _chatikaApiService; + private readonly ILogger _logger; + private readonly ResiliencePipeline _retryPipeline; + + /// + /// توضیحات فارسی فیچر چتیکا + /// + private const string ChatikaFeatureDescription = + "🎉 تبریک! حساب هوش مصنوعی چتیکا شما فعال شد.\n\n" + + "برای استفاده از امکانات رایگان چتیکا:\n" + + "1️⃣ به وب‌سایت chatika.ir مراجعه کنید\n" + + "2️⃣ شماره موبایل خود را وارد کنید\n" + + "3️⃣ از دستیار هوشمند چتیکا لذت ببرید!\n\n" + + "🔗 لینک ورود: https://chatika.ir"; + + public ChatikaAccountActivationJob( + IApplicationDbContext context, + IChatikaApiService chatikaApiService, + ILogger logger) + { + _context = context; + _chatikaApiService = chatikaApiService; + _logger = logger; + + // Polly Retry: 3 تلاش با فاصله نمایی + _retryPipeline = new ResiliencePipelineBuilder() + .AddRetry(new Polly.Retry.RetryStrategyOptions + { + MaxRetryAttempts = 3, + Delay = TimeSpan.FromSeconds(30), + BackoffType = Polly.DelayBackoffType.Exponential, + UseJitter = true, + OnRetry = args => + { + _logger.LogWarning( + "⚠️ Retry attempt {AttemptNumber} for Chatika API. Exception: {ExceptionType}", + args.AttemptNumber, + args.Outcome.Exception?.GetType().Name ?? "None"); + return ValueTask.CompletedTask; + } + }) + .Build(); + } + + /// + /// اجرای Job برای فعال‌سازی حساب چتیکا + /// + public async Task ExecuteAsync(CancellationToken cancellationToken = default) + { + var executionId = Guid.NewGuid(); + _logger.LogInformation( + "🚀 [{ExecutionId}] Starting Chatika account activation job", + executionId); + + try + { + // پیدا کردن کاربرانی که: + // 1. باشگاه فعال دارند (ClubMembership.IsActive = true) + // 2. فیچر چتیکا (Id=1) رو دارند + // 3. فیچر چتیکاشون هنوز Notes نداره (یعنی حساب ساخته نشده) + var pendingUsers = await _context.UserClubFeatures + .Include(ucf => ucf.User) + .Include(ucf => ucf.ClubMembership) + .Where(ucf => + ucf.ClubFeatureId == (long)ClubFeatureType.Chatika && + ucf.ClubMembership.IsActive && + !ucf.IsDeleted && + ucf.IsActive && + (ucf.Notes == null || ucf.Notes == "")) // حساب هنوز ساخته نشده + .ToListAsync(cancellationToken); + + if (!pendingUsers.Any()) + { + _logger.LogInformation( + "✅ [{ExecutionId}] No pending users for Chatika activation", + executionId); + return; + } + + _logger.LogInformation( + "📋 [{ExecutionId}] Found {Count} users pending Chatika activation", + executionId, + pendingUsers.Count); + + var successCount = 0; + var failCount = 0; + + foreach (var userFeature in pendingUsers) + { + try + { + // بررسی تکراری نبودن (Double-check) + if (!string.IsNullOrEmpty(userFeature.Notes)) + { + _logger.LogDebug( + "⏭️ Skipping user {UserId} - already processed", + userFeature.UserId); + continue; + } + + var user = userFeature.User; + var fullName = $"{user.FirstName} {user.LastName}".Trim(); + if (string.IsNullOrWhiteSpace(fullName)) fullName = user.Mobile; + + // کال کردن API چتیکا با retry + var result = await _retryPipeline.ExecuteAsync( + async ct => await _chatikaApiService.CreateAccountAsync( + user.Mobile, + fullName, + ct), + cancellationToken); + + if (result.IsSuccess) + { + // آپدیت فیچر با توضیحات و URL + var description = ChatikaFeatureDescription; + if (!string.IsNullOrEmpty(result.AccessUrl)) + { + description = description.Replace( + "https://chatika.ir", + result.AccessUrl); + } + + userFeature.Notes = description; + userFeature.IsActive = true; + userFeature.LastModified = DateTime.Now; + + await _context.SaveChangesAsync(cancellationToken); + + _logger.LogInformation( + "✅ Chatika account activated for user {UserId}", + userFeature.UserId); + successCount++; + } + else + { + _logger.LogWarning( + "⚠️ Failed to create Chatika account for user {UserId}: {Error}", + userFeature.UserId, + result.ErrorMessage); + failCount++; + } + } + catch (Exception ex) + { + _logger.LogError(ex, + "❌ Error processing Chatika activation for user {UserId}", + userFeature.UserId); + failCount++; + } + } + + _logger.LogInformation( + "🏁 [{ExecutionId}] Chatika activation job completed. Success: {Success}, Failed: {Failed}", + executionId, + successCount, + failCount); + } + catch (Exception ex) + { + _logger.LogError(ex, + "❌ [{ExecutionId}] Chatika activation job failed", + executionId); + throw; + } + } +} diff --git a/src/CMSMicroservice.Infrastructure/ConfigureServices.cs b/src/CMSMicroservice.Infrastructure/ConfigureServices.cs index db75668..496a87e 100644 --- a/src/CMSMicroservice.Infrastructure/ConfigureServices.cs +++ b/src/CMSMicroservice.Infrastructure/ConfigureServices.cs @@ -92,9 +92,19 @@ public static class ConfigureServices // Commission Calculation Strategy Factory - برای سوییچ بین ORM و SP services.AddScoped(); + // Chatika API Service - سرویس ایجاد حساب چتیکا + services.AddHttpClient() + .SetHandlerLifetime(TimeSpan.FromMinutes(5)) + .ConfigureHttpClient((sp, client) => + { + var config = sp.GetRequiredService(); + client.Timeout = TimeSpan.FromSeconds(30); + }); + // Background Workers - Deprecated: Using Hangfire instead // services.AddHostedService(); services.AddScoped(); // Hangfire Job (Scoped for DI) + services.AddScoped(); // Hangfire Job for Chatika activation if (configuration.GetValue("UseInMemoryDatabase")) { diff --git a/src/CMSMicroservice.Infrastructure/Services/ChatikaApiService.cs b/src/CMSMicroservice.Infrastructure/Services/ChatikaApiService.cs new file mode 100644 index 0000000..d4903b7 --- /dev/null +++ b/src/CMSMicroservice.Infrastructure/Services/ChatikaApiService.cs @@ -0,0 +1,156 @@ +using System.Net.Http; +using System.Net.Http.Json; +using System.Text.Json; +using System.Text.Json.Serialization; +using CMSMicroservice.Application.Common.Interfaces; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace CMSMicroservice.Infrastructure.Services; + +/// +/// پیاده‌سازی سرویس چتیکا با HttpClient +/// API: POST /api/v1/organizations/register-user +/// +public class ChatikaApiService : IChatikaApiService +{ + private readonly HttpClient _httpClient; + private readonly ILogger _logger; + private readonly string _baseUrl; + private readonly string _apiKey; + + public ChatikaApiService( + HttpClient httpClient, + ILogger logger, + IConfiguration configuration) + { + _httpClient = httpClient; + _logger = logger; + + // تنظیمات از appsettings.json + _baseUrl = configuration["Chatika:BaseUrl"] ?? "https://api.chatika.ir"; + _apiKey = configuration["Chatika:ApiKey"] ?? ""; + + // تنظیم HttpClient + _httpClient.BaseAddress = new Uri(_baseUrl); + if (!string.IsNullOrEmpty(_apiKey)) + { + _httpClient.DefaultRequestHeaders.Add("X-API-Key", _apiKey); + } + } + + /// + public async Task CreateAccountAsync( + string mobileNumber, + string fullName, + CancellationToken cancellationToken = default) + { + try + { + _logger.LogInformation( + "🤖 Creating Chatika account for mobile: {Mobile}", + mobileNumber.Substring(0, 4) + "***"); + + var request = new ChatikaRegisterRequest + { + MobileNumber = mobileNumber + }; + + var response = await _httpClient.PostAsJsonAsync( + "/api/v1/organizations/register-user", + request, + cancellationToken); + + if (response.IsSuccessStatusCode) + { + var result = await response.Content.ReadFromJsonAsync( + cancellationToken: cancellationToken); + + _logger.LogInformation( + "✅ Chatika account created successfully. UserId: {UserId}, IsNewUser: {IsNew}, Credit: {Credit}", + result?.Id, + result?.IsNewUser, + result?.CreditCharged); + + return ChatikaAccountResult.Success( + chatikaUserId: result?.Id.ToString(), + accessUrl: "https://chatika.ir" + ); + } + + var errorContent = await response.Content.ReadAsStringAsync(cancellationToken); + var errorResponse = JsonSerializer.Deserialize(errorContent); + + _logger.LogWarning( + "⚠️ Chatika API returned error. Status: {Status}, Code: {ErrorCode}, Message: {Message}", + response.StatusCode, + errorResponse?.ErrorCode, + errorResponse?.Message); + + return ChatikaAccountResult.Failure($"{errorResponse?.ErrorCode}: {errorResponse?.Message}"); + } + catch (HttpRequestException ex) + { + _logger.LogError(ex, "❌ Network error calling Chatika API"); + return ChatikaAccountResult.Failure($"خطای شبکه: {ex.Message}"); + } + catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException) + { + _logger.LogError(ex, "❌ Timeout calling Chatika API"); + return ChatikaAccountResult.Failure("تایم‌اوت در ارتباط با سرویس چتیکا"); + } + catch (Exception ex) + { + _logger.LogError(ex, "❌ Unexpected error calling Chatika API"); + return ChatikaAccountResult.Failure($"خطای غیرمنتظره: {ex.Message}"); + } + } + + /// + /// Request model برای ثبت کاربر در چتیکا + /// + private class ChatikaRegisterRequest + { + [JsonPropertyName("mobile_number")] + public string MobileNumber { get; set; } = string.Empty; + } + + /// + /// Response model موفق از چتیکا + /// + private class ChatikaRegisterResponse + { + [JsonPropertyName("id")] + public long Id { get; set; } + + [JsonPropertyName("mobile_number")] + public string MobileNumber { get; set; } = string.Empty; + + [JsonPropertyName("organization_id")] + public long OrganizationId { get; set; } + + [JsonPropertyName("organization_title")] + public string OrganizationTitle { get; set; } = string.Empty; + + [JsonPropertyName("wallet_balance")] + public decimal WalletBalance { get; set; } + + [JsonPropertyName("is_new_user")] + public bool IsNewUser { get; set; } + + [JsonPropertyName("credit_charged")] + public decimal CreditCharged { get; set; } + } + + /// + /// Response model خطا از چتیکا + /// + private class ChatikaErrorResponse + { + [JsonPropertyName("error_code")] + public string ErrorCode { get; set; } = string.Empty; + + [JsonPropertyName("message")] + public string Message { get; set; } = string.Empty; + } +} diff --git a/src/CMSMicroservice.WebApi/Program.cs b/src/CMSMicroservice.WebApi/Program.cs index 5017580..7eeae10 100644 --- a/src/CMSMicroservice.WebApi/Program.cs +++ b/src/CMSMicroservice.WebApi/Program.cs @@ -223,6 +223,14 @@ using (var scope = app.Services.CreateScope()) // Daya Loan Check: Every 15 minutes CMSMicroservice.WebApi.Workers.DayaLoanCheckWorker.Schedule(recurringJobManager); app.Logger.LogInformation("✅ Hangfire recurring job 'daya-loan-check' registered (Cron: */15 * * * * - Every 15 minutes)"); + + // Chatika Account Activation: Every 5 minutes + recurringJobManager.AddOrUpdate( + recurringJobId: "chatika-account-activation", + methodCall: job => job.ExecuteAsync(CancellationToken.None), + cronExpression: "*/5 * * * *", + options: new RecurringJobOptions { TimeZone = TimeZoneInfo.Utc }); + app.Logger.LogInformation("✅ Hangfire recurring job 'chatika-account-activation' registered (Cron: */5 * * * * - Every 5 minutes)"); } app.Run(); diff --git a/src/CMSMicroservice.WebApi/appsettings.json b/src/CMSMicroservice.WebApi/appsettings.json index 5d594c6..c78acf9 100644 --- a/src/CMSMicroservice.WebApi/appsettings.json +++ b/src/CMSMicroservice.WebApi/appsettings.json @@ -50,6 +50,10 @@ "MerchantPermissionKey": "56146364$04sXjethI5WxhItR1Q9xnmFdJzl2BB8Bclsq8dAy7YVSZp3vtt-wP7ivrcCvmKLq", "CacheDurationMinutes": 20 }, + "Chatika": { + "BaseUrl": "https://api.chatika.ir", + "ApiKey": "tIukvL8dnV4cB3yVWcCD9Xyfbj8rBxm5wPt2mLyJCgTsBBoMTWjt6mFEqQwpw-er" + }, "AllowedHosts": "*", "Kestrel": { "EndpointDefaults": {