Add customer communication system

This commit is contained in:
sysadmin
2025-08-27 18:02:39 +01:00
parent 1f7c0af497
commit eae5be3e7c
136 changed files with 14552 additions and 97 deletions

View File

@@ -73,6 +73,21 @@ public static class ServiceCollectionExtensions
return new RetryPolicyHandler(logger, options.MaxRetryAttempts);
});
services.AddHttpClient<ICustomerService, CustomerService>((serviceProvider, client) =>
{
var options = serviceProvider.GetRequiredService<IOptions<LittleShopClientOptions>>().Value;
client.BaseAddress = new Uri(options.BaseUrl);
client.Timeout = TimeSpan.FromSeconds(options.TimeoutSeconds);
client.DefaultRequestHeaders.Add("Accept", "application/json");
})
.AddHttpMessageHandler<ErrorHandlingMiddleware>()
.AddHttpMessageHandler(serviceProvider =>
{
var logger = serviceProvider.GetRequiredService<ILogger<RetryPolicyHandler>>();
var options = serviceProvider.GetRequiredService<IOptions<LittleShopClientOptions>>().Value;
return new RetryPolicyHandler(logger, options.MaxRetryAttempts);
});
// Register the main client
services.AddScoped<ILittleShopClient, LittleShopClient>();

View File

@@ -9,6 +9,7 @@ public interface ILittleShopClient
IAuthenticationService Authentication { get; }
ICatalogService Catalog { get; }
IOrderService Orders { get; }
ICustomerService Customers { get; }
}
public class LittleShopClient : ILittleShopClient
@@ -16,14 +17,17 @@ public class LittleShopClient : ILittleShopClient
public IAuthenticationService Authentication { get; }
public ICatalogService Catalog { get; }
public IOrderService Orders { get; }
public ICustomerService Customers { get; }
public LittleShopClient(
IAuthenticationService authenticationService,
ICatalogService catalogService,
IOrderService orderService)
IOrderService orderService,
ICustomerService customerService)
{
Authentication = authenticationService;
Catalog = catalogService;
Orders = orderService;
Customers = customerService;
}
}

View File

@@ -37,4 +37,6 @@ public class PagedResult<T>
public int PageNumber { get; set; }
public int PageSize { get; set; }
public int TotalPages => (int)Math.Ceiling(TotalCount / (double)PageSize);
public bool HasPreviousPage => PageNumber > 1;
public bool HasNextPage => PageNumber < TotalPages;
}

View File

@@ -0,0 +1,68 @@
namespace LittleShop.Client.Models;
public class Customer
{
public Guid Id { get; set; }
public long TelegramUserId { get; set; }
public string TelegramUsername { get; set; } = string.Empty;
public string TelegramDisplayName { get; set; } = string.Empty;
public string TelegramFirstName { get; set; } = string.Empty;
public string TelegramLastName { get; set; } = string.Empty;
public string? Email { get; set; }
public string? PhoneNumber { get; set; }
public bool AllowMarketing { get; set; }
public bool AllowOrderUpdates { get; set; }
public string Language { get; set; } = "en";
public string Timezone { get; set; } = "UTC";
public int TotalOrders { get; set; }
public decimal TotalSpent { get; set; }
public decimal AverageOrderValue { get; set; }
public DateTime FirstOrderDate { get; set; }
public DateTime LastOrderDate { get; set; }
public string? CustomerNotes { get; set; }
public bool IsBlocked { get; set; }
public string? BlockReason { get; set; }
public int RiskScore { get; set; }
public int SuccessfulOrders { get; set; }
public int CancelledOrders { get; set; }
public int DisputedOrders { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
public DateTime LastActiveAt { get; set; }
public DateTime? DataRetentionDate { get; set; }
public bool IsActive { get; set; }
public string DisplayName { get; set; } = string.Empty;
public string CustomerType { get; set; } = string.Empty;
}
public class CreateCustomerRequest
{
public long TelegramUserId { get; set; }
public string TelegramUsername { get; set; } = string.Empty;
public string TelegramDisplayName { get; set; } = string.Empty;
public string TelegramFirstName { get; set; } = string.Empty;
public string TelegramLastName { get; set; } = string.Empty;
public string? Email { get; set; }
public string? PhoneNumber { get; set; }
public bool AllowMarketing { get; set; } = false;
public bool AllowOrderUpdates { get; set; } = true;
public string Language { get; set; } = "en";
public string Timezone { get; set; } = "UTC";
}
public class UpdateCustomerRequest
{
public string TelegramUsername { get; set; } = string.Empty;
public string TelegramDisplayName { get; set; } = string.Empty;
public string TelegramFirstName { get; set; } = string.Empty;
public string TelegramLastName { get; set; } = string.Empty;
public string? Email { get; set; }
public string? PhoneNumber { get; set; }
public bool AllowMarketing { get; set; }
public bool AllowOrderUpdates { get; set; }
public string Language { get; set; } = "en";
public string Timezone { get; set; } = "UTC";
public string? CustomerNotes { get; set; }
public bool IsBlocked { get; set; }
public string? BlockReason { get; set; }
}

View File

@@ -3,8 +3,9 @@ namespace LittleShop.Client.Models;
public class Order
{
public Guid Id { get; set; }
public string IdentityReference { get; set; } = string.Empty;
public string Status { get; set; } = string.Empty;
public Guid? CustomerId { get; set; }
public string? IdentityReference { get; set; }
public int Status { get; set; }
public decimal TotalAmount { get; set; }
public string Currency { get; set; } = string.Empty;
public string ShippingName { get; set; } = string.Empty;
@@ -34,7 +35,13 @@ public class OrderItem
public class CreateOrderRequest
{
public string IdentityReference { get; set; } = string.Empty;
// Either Customer ID (for registered customers) OR Identity Reference (for anonymous)
public Guid? CustomerId { get; set; }
public string? IdentityReference { get; set; }
// Customer Information (collected at checkout for new customers)
public CreateCustomerRequest? CustomerInfo { get; set; }
public string ShippingName { get; set; } = string.Empty;
public string ShippingAddress { get; set; } = string.Empty;
public string ShippingCity { get; set; } = string.Empty;

View File

@@ -4,11 +4,11 @@ public class CryptoPayment
{
public Guid Id { get; set; }
public Guid OrderId { get; set; }
public string Currency { get; set; } = string.Empty;
public int Currency { get; set; }
public string WalletAddress { get; set; } = string.Empty;
public decimal RequiredAmount { get; set; }
public decimal? PaidAmount { get; set; }
public string Status { get; set; } = string.Empty;
public int Status { get; set; }
public string? TransactionHash { get; set; }
public string? BTCPayInvoiceId { get; set; }
public string? BTCPayCheckoutUrl { get; set; }
@@ -19,5 +19,5 @@ public class CryptoPayment
public class CreatePaymentRequest
{
public string Currency { get; set; } = "BTC";
public int Currency { get; set; } = 0; // BTC = 0
}

View File

@@ -7,7 +7,7 @@ public class Product
public string? Description { get; set; }
public decimal Price { get; set; }
public decimal Weight { get; set; }
public string WeightUnit { get; set; } = string.Empty;
public int WeightUnit { get; set; }
public Guid CategoryId { get; set; }
public string? CategoryName { get; set; }
public bool IsActive { get; set; }

View File

@@ -0,0 +1,183 @@
using System.Net.Http.Json;
using Microsoft.Extensions.Logging;
using LittleShop.Client.Models;
namespace LittleShop.Client.Services;
public class CustomerService : ICustomerService
{
private readonly HttpClient _httpClient;
private readonly ILogger<CustomerService> _logger;
public CustomerService(HttpClient httpClient, ILogger<CustomerService> logger)
{
_httpClient = httpClient;
_logger = logger;
}
public async Task<ApiResponse<Customer>> GetCustomerByIdAsync(Guid id)
{
try
{
var response = await _httpClient.GetAsync($"api/customers/{id}");
if (response.IsSuccessStatusCode)
{
var customer = await response.Content.ReadFromJsonAsync<Customer>();
return ApiResponse<Customer>.Success(customer!);
}
var error = await response.Content.ReadAsStringAsync();
return ApiResponse<Customer>.Failure(error, response.StatusCode);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to get customer {CustomerId}", id);
return ApiResponse<Customer>.Failure(
ex.Message,
System.Net.HttpStatusCode.InternalServerError);
}
}
public async Task<ApiResponse<Customer>> GetCustomerByTelegramUserIdAsync(long telegramUserId)
{
try
{
var response = await _httpClient.GetAsync($"api/customers/by-telegram/{telegramUserId}");
if (response.IsSuccessStatusCode)
{
var customer = await response.Content.ReadFromJsonAsync<Customer>();
return ApiResponse<Customer>.Success(customer!);
}
var error = await response.Content.ReadAsStringAsync();
return ApiResponse<Customer>.Failure(error, response.StatusCode);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to get customer by Telegram ID {TelegramUserId}", telegramUserId);
return ApiResponse<Customer>.Failure(
ex.Message,
System.Net.HttpStatusCode.InternalServerError);
}
}
public async Task<ApiResponse<Customer>> CreateCustomerAsync(CreateCustomerRequest request)
{
try
{
var response = await _httpClient.PostAsJsonAsync("api/customers", request);
if (response.IsSuccessStatusCode)
{
var customer = await response.Content.ReadFromJsonAsync<Customer>();
return ApiResponse<Customer>.Success(customer!);
}
var error = await response.Content.ReadAsStringAsync();
return ApiResponse<Customer>.Failure(error, response.StatusCode);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to create customer");
return ApiResponse<Customer>.Failure(
ex.Message,
System.Net.HttpStatusCode.InternalServerError);
}
}
public async Task<ApiResponse<Customer>> GetOrCreateCustomerAsync(CreateCustomerRequest request)
{
try
{
var response = await _httpClient.PostAsJsonAsync("api/customers/get-or-create", request);
if (response.IsSuccessStatusCode)
{
var customer = await response.Content.ReadFromJsonAsync<Customer>();
return ApiResponse<Customer>.Success(customer!);
}
var error = await response.Content.ReadAsStringAsync();
return ApiResponse<Customer>.Failure(error, response.StatusCode);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to get or create customer");
return ApiResponse<Customer>.Failure(
ex.Message,
System.Net.HttpStatusCode.InternalServerError);
}
}
public async Task<ApiResponse<Customer>> UpdateCustomerAsync(Guid id, UpdateCustomerRequest request)
{
try
{
var response = await _httpClient.PutAsJsonAsync($"api/customers/{id}", request);
if (response.IsSuccessStatusCode)
{
var customer = await response.Content.ReadFromJsonAsync<Customer>();
return ApiResponse<Customer>.Success(customer!);
}
var error = await response.Content.ReadAsStringAsync();
return ApiResponse<Customer>.Failure(error, response.StatusCode);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to update customer {CustomerId}", id);
return ApiResponse<Customer>.Failure(
ex.Message,
System.Net.HttpStatusCode.InternalServerError);
}
}
public async Task<ApiResponse<bool>> BlockCustomerAsync(Guid id, string reason)
{
try
{
var response = await _httpClient.PostAsJsonAsync($"api/customers/{id}/block", reason);
if (response.IsSuccessStatusCode)
{
return ApiResponse<bool>.Success(true);
}
var error = await response.Content.ReadAsStringAsync();
return ApiResponse<bool>.Failure(error, response.StatusCode);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to block customer {CustomerId}", id);
return ApiResponse<bool>.Failure(
ex.Message,
System.Net.HttpStatusCode.InternalServerError);
}
}
public async Task<ApiResponse<bool>> UnblockCustomerAsync(Guid id)
{
try
{
var response = await _httpClient.PostAsync($"api/customers/{id}/unblock", null);
if (response.IsSuccessStatusCode)
{
return ApiResponse<bool>.Success(true);
}
var error = await response.Content.ReadAsStringAsync();
return ApiResponse<bool>.Failure(error, response.StatusCode);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to unblock customer {CustomerId}", id);
return ApiResponse<bool>.Failure(
ex.Message,
System.Net.HttpStatusCode.InternalServerError);
}
}
}

View File

@@ -0,0 +1,14 @@
using LittleShop.Client.Models;
namespace LittleShop.Client.Services;
public interface ICustomerService
{
Task<ApiResponse<Customer>> GetCustomerByIdAsync(Guid id);
Task<ApiResponse<Customer>> GetCustomerByTelegramUserIdAsync(long telegramUserId);
Task<ApiResponse<Customer>> CreateCustomerAsync(CreateCustomerRequest request);
Task<ApiResponse<Customer>> GetOrCreateCustomerAsync(CreateCustomerRequest request);
Task<ApiResponse<Customer>> UpdateCustomerAsync(Guid id, UpdateCustomerRequest request);
Task<ApiResponse<bool>> BlockCustomerAsync(Guid id, string reason);
Task<ApiResponse<bool>> UnblockCustomerAsync(Guid id);
}

View File

@@ -7,6 +7,6 @@ public interface IOrderService
Task<ApiResponse<Order>> CreateOrderAsync(CreateOrderRequest request);
Task<ApiResponse<List<Order>>> GetOrdersByIdentityAsync(string identityReference);
Task<ApiResponse<Order>> GetOrderByIdAsync(Guid id);
Task<ApiResponse<CryptoPayment>> CreatePaymentAsync(Guid orderId, string currency);
Task<ApiResponse<CryptoPayment>> CreatePaymentAsync(Guid orderId, int currency);
Task<ApiResponse<List<CryptoPayment>>> GetOrderPaymentsAsync(Guid orderId);
}

View File

@@ -90,7 +90,7 @@ public class OrderService : IOrderService
}
}
public async Task<ApiResponse<CryptoPayment>> CreatePaymentAsync(Guid orderId, string currency)
public async Task<ApiResponse<CryptoPayment>> CreatePaymentAsync(Guid orderId, int currency)
{
try
{