Add customer communication system
This commit is contained in:
58
LittleShop/Models/Bot.cs
Normal file
58
LittleShop/Models/Bot.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using LittleShop.Enums;
|
||||
|
||||
namespace LittleShop.Models;
|
||||
|
||||
public class Bot
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
|
||||
[Required]
|
||||
[StringLength(256)]
|
||||
public string BotKey { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
[StringLength(100)]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[StringLength(500)]
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
public BotType Type { get; set; }
|
||||
|
||||
public BotStatus Status { get; set; }
|
||||
|
||||
public string Settings { get; set; } = "{}"; // JSON storage
|
||||
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
public DateTime? LastSeenAt { get; set; }
|
||||
|
||||
public DateTime? LastConfigSyncAt { get; set; }
|
||||
|
||||
public bool IsActive { get; set; }
|
||||
|
||||
[StringLength(50)]
|
||||
public string Version { get; set; } = string.Empty;
|
||||
|
||||
[StringLength(50)]
|
||||
public string IpAddress { get; set; } = string.Empty;
|
||||
|
||||
[StringLength(100)]
|
||||
public string PlatformUsername { get; set; } = string.Empty;
|
||||
|
||||
[StringLength(200)]
|
||||
public string PlatformDisplayName { get; set; } = string.Empty;
|
||||
|
||||
[StringLength(100)]
|
||||
public string PlatformId { get; set; } = string.Empty;
|
||||
|
||||
[StringLength(50)]
|
||||
public string PersonalityName { get; set; } = string.Empty;
|
||||
|
||||
// Navigation properties
|
||||
public virtual ICollection<BotMetric> Metrics { get; set; } = new List<BotMetric>();
|
||||
public virtual ICollection<BotSession> Sessions { get; set; } = new List<BotSession>();
|
||||
}
|
||||
30
LittleShop/Models/BotMetric.cs
Normal file
30
LittleShop/Models/BotMetric.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using LittleShop.Enums;
|
||||
|
||||
namespace LittleShop.Models;
|
||||
|
||||
public class BotMetric
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
|
||||
[Required]
|
||||
public Guid BotId { get; set; }
|
||||
|
||||
public MetricType MetricType { get; set; }
|
||||
|
||||
public decimal Value { get; set; }
|
||||
|
||||
public string Metadata { get; set; } = "{}"; // JSON storage for additional data
|
||||
|
||||
public DateTime RecordedAt { get; set; }
|
||||
|
||||
[StringLength(100)]
|
||||
public string Category { get; set; } = string.Empty;
|
||||
|
||||
[StringLength(500)]
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
// Navigation property
|
||||
public virtual Bot Bot { get; set; } = null!;
|
||||
}
|
||||
44
LittleShop/Models/BotSession.cs
Normal file
44
LittleShop/Models/BotSession.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace LittleShop.Models;
|
||||
|
||||
public class BotSession
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
|
||||
[Required]
|
||||
public Guid BotId { get; set; }
|
||||
|
||||
[Required]
|
||||
[StringLength(256)]
|
||||
public string SessionIdentifier { get; set; } = string.Empty; // Hashed user ID
|
||||
|
||||
[StringLength(100)]
|
||||
public string Platform { get; set; } = string.Empty; // Telegram, Discord, etc.
|
||||
|
||||
public DateTime StartedAt { get; set; }
|
||||
|
||||
public DateTime LastActivityAt { get; set; }
|
||||
|
||||
public DateTime? EndedAt { get; set; }
|
||||
|
||||
public int OrderCount { get; set; }
|
||||
|
||||
public int MessageCount { get; set; }
|
||||
|
||||
public decimal TotalSpent { get; set; }
|
||||
|
||||
[StringLength(50)]
|
||||
public string Language { get; set; } = "en";
|
||||
|
||||
[StringLength(100)]
|
||||
public string Country { get; set; } = string.Empty;
|
||||
|
||||
public bool IsAnonymous { get; set; }
|
||||
|
||||
public string Metadata { get; set; } = "{}"; // JSON for additional session data
|
||||
|
||||
// Navigation property
|
||||
public virtual Bot Bot { get; set; } = null!;
|
||||
}
|
||||
148
LittleShop/Models/Customer.cs
Normal file
148
LittleShop/Models/Customer.cs
Normal file
@@ -0,0 +1,148 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace LittleShop.Models;
|
||||
|
||||
public class Customer
|
||||
{
|
||||
[Key]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
// Telegram Information
|
||||
[Required]
|
||||
public long TelegramUserId { get; set; }
|
||||
|
||||
[StringLength(100)]
|
||||
public string TelegramUsername { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
[StringLength(200)]
|
||||
public string TelegramDisplayName { get; set; } = string.Empty;
|
||||
|
||||
[StringLength(50)]
|
||||
public string TelegramFirstName { get; set; } = string.Empty;
|
||||
|
||||
[StringLength(50)]
|
||||
public string TelegramLastName { get; set; } = string.Empty;
|
||||
|
||||
// Optional Contact Information (if customer provides)
|
||||
[StringLength(100)]
|
||||
public string? Email { get; set; }
|
||||
|
||||
[StringLength(20)]
|
||||
public string? PhoneNumber { get; set; }
|
||||
|
||||
// Customer Preferences
|
||||
public bool AllowMarketing { get; set; } = false;
|
||||
|
||||
public bool AllowOrderUpdates { get; set; } = true;
|
||||
|
||||
[StringLength(10)]
|
||||
public string Language { get; set; } = "en";
|
||||
|
||||
[StringLength(10)]
|
||||
public string Timezone { get; set; } = "UTC";
|
||||
|
||||
// Customer Metrics
|
||||
public int TotalOrders { get; set; } = 0;
|
||||
|
||||
[Column(TypeName = "decimal(18,2)")]
|
||||
public decimal TotalSpent { get; set; } = 0;
|
||||
|
||||
[Column(TypeName = "decimal(18,2)")]
|
||||
public decimal AverageOrderValue { get; set; } = 0;
|
||||
|
||||
public DateTime FirstOrderDate { get; set; }
|
||||
|
||||
public DateTime LastOrderDate { get; set; }
|
||||
|
||||
// Customer Service Notes
|
||||
[StringLength(2000)]
|
||||
public string? CustomerNotes { get; set; }
|
||||
|
||||
public bool IsBlocked { get; set; } = false;
|
||||
|
||||
[StringLength(500)]
|
||||
public string? BlockReason { get; set; }
|
||||
|
||||
// Risk Assessment
|
||||
public int RiskScore { get; set; } = 0; // 0-100, 0 = trusted, 100 = high risk
|
||||
|
||||
public int SuccessfulOrders { get; set; } = 0;
|
||||
|
||||
public int CancelledOrders { get; set; } = 0;
|
||||
|
||||
public int DisputedOrders { get; set; } = 0;
|
||||
|
||||
// Data Management
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public DateTime LastActiveAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public DateTime? DataRetentionDate { get; set; } // When to delete this customer's data
|
||||
|
||||
public bool IsActive { get; set; } = true;
|
||||
|
||||
// Navigation properties
|
||||
public virtual ICollection<Order> Orders { get; set; } = new List<Order>();
|
||||
public virtual ICollection<CustomerMessage> Messages { get; set; } = new List<CustomerMessage>();
|
||||
|
||||
// Calculated Properties
|
||||
public string DisplayName =>
|
||||
!string.IsNullOrEmpty(TelegramDisplayName) ? TelegramDisplayName :
|
||||
!string.IsNullOrEmpty(TelegramUsername) ? $"@{TelegramUsername}" :
|
||||
$"{TelegramFirstName} {TelegramLastName}".Trim();
|
||||
|
||||
public string CustomerType =>
|
||||
TotalOrders == 0 ? "New" :
|
||||
TotalOrders == 1 ? "First-time" :
|
||||
TotalOrders < 5 ? "Regular" :
|
||||
TotalOrders < 20 ? "Loyal" : "VIP";
|
||||
|
||||
public void UpdateMetrics()
|
||||
{
|
||||
if (Orders?.Any() == true)
|
||||
{
|
||||
TotalOrders = Orders.Count();
|
||||
TotalSpent = Orders.Sum(o => o.TotalAmount);
|
||||
AverageOrderValue = TotalOrders > 0 ? TotalSpent / TotalOrders : 0;
|
||||
FirstOrderDate = Orders.Min(o => o.CreatedAt);
|
||||
LastOrderDate = Orders.Max(o => o.CreatedAt);
|
||||
SuccessfulOrders = Orders.Count(o => o.Status == Enums.OrderStatus.Delivered);
|
||||
CancelledOrders = Orders.Count(o => o.Status == Enums.OrderStatus.Cancelled);
|
||||
}
|
||||
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
// Update risk score based on order history
|
||||
CalculateRiskScore();
|
||||
}
|
||||
|
||||
private void CalculateRiskScore()
|
||||
{
|
||||
int score = 0;
|
||||
|
||||
// New customers have moderate risk
|
||||
if (TotalOrders == 0) score += 30;
|
||||
|
||||
// High cancellation rate increases risk
|
||||
if (TotalOrders > 0)
|
||||
{
|
||||
var cancellationRate = (double)CancelledOrders / TotalOrders;
|
||||
if (cancellationRate > 0.5) score += 40;
|
||||
else if (cancellationRate > 0.3) score += 20;
|
||||
}
|
||||
|
||||
// Disputes increase risk significantly
|
||||
score += DisputedOrders * 25;
|
||||
|
||||
// Long-term customers with good history reduce risk
|
||||
if (TotalOrders > 10 && SuccessfulOrders > 8) score -= 20;
|
||||
if (TotalSpent > 500) score -= 10;
|
||||
|
||||
// Clamp between 0-100
|
||||
RiskScore = Math.Max(0, Math.Min(100, score));
|
||||
}
|
||||
}
|
||||
185
LittleShop/Models/CustomerMessage.cs
Normal file
185
LittleShop/Models/CustomerMessage.cs
Normal file
@@ -0,0 +1,185 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace LittleShop.Models;
|
||||
|
||||
public enum MessageDirection
|
||||
{
|
||||
AdminToCustomer = 0,
|
||||
CustomerToAdmin = 1
|
||||
}
|
||||
|
||||
public enum MessageStatus
|
||||
{
|
||||
Pending = 0, // Message created, waiting to be sent
|
||||
Sent = 1, // Message delivered to customer
|
||||
Delivered = 2, // Message acknowledged by bot/platform
|
||||
Read = 3, // Customer has read the message
|
||||
Failed = 4 // Delivery failed
|
||||
}
|
||||
|
||||
public enum MessageType
|
||||
{
|
||||
OrderUpdate = 0, // Status update about an order
|
||||
PaymentReminder = 1, // Payment reminder
|
||||
ShippingInfo = 2, // Tracking/shipping information
|
||||
CustomerService = 3, // General customer service
|
||||
Marketing = 4, // Marketing/promotional (requires consent)
|
||||
SystemAlert = 5 // System-generated alerts
|
||||
}
|
||||
|
||||
public class CustomerMessage
|
||||
{
|
||||
[Key]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
// Relationships
|
||||
[Required]
|
||||
public Guid CustomerId { get; set; }
|
||||
|
||||
public Guid? OrderId { get; set; } // Optional - message may not be about specific order
|
||||
|
||||
public Guid? AdminUserId { get; set; } // Which admin sent the message (null for system messages or unidentified admins)
|
||||
|
||||
// Message Details
|
||||
[Required]
|
||||
public MessageDirection Direction { get; set; }
|
||||
|
||||
[Required]
|
||||
public MessageType Type { get; set; }
|
||||
|
||||
[Required]
|
||||
[StringLength(100)]
|
||||
public string Subject { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
[StringLength(4000)] // Telegram message limit is ~4096 characters
|
||||
public string Content { get; set; } = string.Empty;
|
||||
|
||||
public MessageStatus Status { get; set; } = MessageStatus.Pending;
|
||||
|
||||
// Delivery Information
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public DateTime? SentAt { get; set; }
|
||||
|
||||
public DateTime? DeliveredAt { get; set; }
|
||||
|
||||
public DateTime? ReadAt { get; set; }
|
||||
|
||||
public DateTime? FailedAt { get; set; }
|
||||
|
||||
[StringLength(500)]
|
||||
public string? FailureReason { get; set; }
|
||||
|
||||
public int RetryCount { get; set; } = 0;
|
||||
|
||||
public DateTime? NextRetryAt { get; set; }
|
||||
|
||||
// Message Threading (for conversations)
|
||||
public Guid? ParentMessageId { get; set; } // Reply to another message
|
||||
|
||||
public Guid? ThreadId { get; set; } // Conversation thread identifier
|
||||
|
||||
// Platform-specific Information
|
||||
[StringLength(100)]
|
||||
public string Platform { get; set; } = "Telegram"; // Telegram, Email, SMS, etc.
|
||||
|
||||
[StringLength(200)]
|
||||
public string? PlatformMessageId { get; set; } // Telegram message ID, email message ID, etc.
|
||||
|
||||
// Priority and Scheduling
|
||||
public int Priority { get; set; } = 5; // 1-10, 1 = highest priority
|
||||
|
||||
public DateTime? ScheduledFor { get; set; } // For scheduled messages
|
||||
|
||||
public DateTime? ExpiresAt { get; set; } // Message expires if not delivered
|
||||
|
||||
// Customer Preferences
|
||||
public bool RequiresResponse { get; set; } = false;
|
||||
|
||||
public bool IsUrgent { get; set; } = false;
|
||||
|
||||
public bool IsMarketing { get; set; } = false;
|
||||
|
||||
// Auto-generated flags
|
||||
public bool IsAutoGenerated { get; set; } = false;
|
||||
|
||||
[StringLength(100)]
|
||||
public string? AutoGenerationTrigger { get; set; } // e.g., "OrderStatusChanged", "PaymentOverdue"
|
||||
|
||||
// Data retention
|
||||
public bool IsArchived { get; set; } = false;
|
||||
|
||||
public DateTime? ArchivedAt { get; set; }
|
||||
|
||||
// Navigation properties
|
||||
public virtual Customer Customer { get; set; } = null!;
|
||||
public virtual Order? Order { get; set; }
|
||||
public virtual User? AdminUser { get; set; }
|
||||
public virtual CustomerMessage? ParentMessage { get; set; }
|
||||
public virtual ICollection<CustomerMessage> Replies { get; set; } = new List<CustomerMessage>();
|
||||
|
||||
// Helper methods
|
||||
public bool CanRetry()
|
||||
{
|
||||
return Status == MessageStatus.Failed &&
|
||||
RetryCount < 3 &&
|
||||
(NextRetryAt == null || DateTime.UtcNow >= NextRetryAt) &&
|
||||
(ExpiresAt == null || DateTime.UtcNow < ExpiresAt);
|
||||
}
|
||||
|
||||
public void MarkAsSent()
|
||||
{
|
||||
Status = MessageStatus.Sent;
|
||||
SentAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public void MarkAsDelivered(string? platformMessageId = null)
|
||||
{
|
||||
Status = MessageStatus.Delivered;
|
||||
DeliveredAt = DateTime.UtcNow;
|
||||
if (!string.IsNullOrEmpty(platformMessageId))
|
||||
{
|
||||
PlatformMessageId = platformMessageId;
|
||||
}
|
||||
}
|
||||
|
||||
public void MarkAsRead()
|
||||
{
|
||||
Status = MessageStatus.Read;
|
||||
ReadAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public void MarkAsFailed(string reason)
|
||||
{
|
||||
Status = MessageStatus.Failed;
|
||||
FailedAt = DateTime.UtcNow;
|
||||
FailureReason = reason;
|
||||
RetryCount++;
|
||||
|
||||
// Exponential backoff for retries
|
||||
if (RetryCount <= 3)
|
||||
{
|
||||
var delayMinutes = Math.Pow(2, RetryCount) * 5; // 10, 20, 40 minutes
|
||||
NextRetryAt = DateTime.UtcNow.AddMinutes(delayMinutes);
|
||||
}
|
||||
}
|
||||
|
||||
public string GetDisplayTitle()
|
||||
{
|
||||
var prefix = Direction == MessageDirection.AdminToCustomer ? "→" : "←";
|
||||
var typeStr = Type switch
|
||||
{
|
||||
MessageType.OrderUpdate => "Order Update",
|
||||
MessageType.PaymentReminder => "Payment",
|
||||
MessageType.ShippingInfo => "Shipping",
|
||||
MessageType.CustomerService => "Support",
|
||||
MessageType.Marketing => "Marketing",
|
||||
MessageType.SystemAlert => "System",
|
||||
_ => "Message"
|
||||
};
|
||||
|
||||
return $"{prefix} {typeStr}: {Subject}";
|
||||
}
|
||||
}
|
||||
@@ -9,9 +9,12 @@ public class Order
|
||||
[Key]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
[Required]
|
||||
// Customer Information (nullable for transition period)
|
||||
public Guid? CustomerId { get; set; }
|
||||
|
||||
// Legacy identity reference (still used for anonymous orders)
|
||||
[StringLength(100)]
|
||||
public string IdentityReference { get; set; } = string.Empty;
|
||||
public string? IdentityReference { get; set; }
|
||||
|
||||
public OrderStatus Status { get; set; } = OrderStatus.PendingPayment;
|
||||
|
||||
@@ -57,6 +60,7 @@ public class Order
|
||||
public DateTime? ShippedAt { get; set; }
|
||||
|
||||
// Navigation properties
|
||||
public virtual Customer? Customer { get; set; }
|
||||
public virtual ICollection<OrderItem> Items { get; set; } = new List<OrderItem>();
|
||||
public virtual ICollection<CryptoPayment> Payments { get; set; } = new List<CryptoPayment>();
|
||||
}
|
||||
Reference in New Issue
Block a user