feat: implement Chatika account activation job for new club members
This commit is contained in:
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Hangfire Job برای فعالسازی حساب چتیکا برای اعضای جدید باشگاه
|
||||
/// این Job کاربرانی که باشگاهشان فعال شده ولی حساب چتیکا ندارند را پیدا کرده و حساب میسازد
|
||||
/// </summary>
|
||||
public class ChatikaAccountActivationJob
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly IChatikaApiService _chatikaApiService;
|
||||
private readonly ILogger<ChatikaAccountActivationJob> _logger;
|
||||
private readonly ResiliencePipeline _retryPipeline;
|
||||
|
||||
/// <summary>
|
||||
/// توضیحات فارسی فیچر چتیکا
|
||||
/// </summary>
|
||||
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<ChatikaAccountActivationJob> 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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// اجرای Job برای فعالسازی حساب چتیکا
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -92,9 +92,19 @@ public static class ConfigureServices
|
||||
// Commission Calculation Strategy Factory - برای سوییچ بین ORM و SP
|
||||
services.AddScoped<ICommissionCalculationStrategyFactory, CommissionCalculationStrategyFactory>();
|
||||
|
||||
// Chatika API Service - سرویس ایجاد حساب چتیکا
|
||||
services.AddHttpClient<IChatikaApiService, ChatikaApiService>()
|
||||
.SetHandlerLifetime(TimeSpan.FromMinutes(5))
|
||||
.ConfigureHttpClient((sp, client) =>
|
||||
{
|
||||
var config = sp.GetRequiredService<IConfiguration>();
|
||||
client.Timeout = TimeSpan.FromSeconds(30);
|
||||
});
|
||||
|
||||
// Background Workers - Deprecated: Using Hangfire instead
|
||||
// services.AddHostedService<WeeklyNetworkCommissionWorker>();
|
||||
services.AddScoped<WeeklyCommissionJob>(); // Hangfire Job (Scoped for DI)
|
||||
services.AddScoped<ChatikaAccountActivationJob>(); // Hangfire Job for Chatika activation
|
||||
|
||||
if (configuration.GetValue<bool>("UseInMemoryDatabase"))
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// پیادهسازی سرویس چتیکا با HttpClient
|
||||
/// API: POST /api/v1/organizations/register-user
|
||||
/// </summary>
|
||||
public class ChatikaApiService : IChatikaApiService
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly ILogger<ChatikaApiService> _logger;
|
||||
private readonly string _baseUrl;
|
||||
private readonly string _apiKey;
|
||||
|
||||
public ChatikaApiService(
|
||||
HttpClient httpClient,
|
||||
ILogger<ChatikaApiService> 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<ChatikaAccountResult> 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<ChatikaRegisterResponse>(
|
||||
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<ChatikaErrorResponse>(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}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request model برای ثبت کاربر در چتیکا
|
||||
/// </summary>
|
||||
private class ChatikaRegisterRequest
|
||||
{
|
||||
[JsonPropertyName("mobile_number")]
|
||||
public string MobileNumber { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Response model موفق از چتیکا
|
||||
/// </summary>
|
||||
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; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Response model خطا از چتیکا
|
||||
/// </summary>
|
||||
private class ChatikaErrorResponse
|
||||
{
|
||||
[JsonPropertyName("error_code")]
|
||||
public string ErrorCode { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("message")]
|
||||
public string Message { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user