541 lines
20 KiB
C#
541 lines
20 KiB
C#
using FsCheck;
|
|
using FsCheck.Xunit;
|
|
using MiAssessment.Core.Interfaces;
|
|
using MiAssessment.Core.Services;
|
|
using MiAssessment.Model.Entities;
|
|
using Microsoft.Extensions.Logging;
|
|
using Moq;
|
|
using Xunit;
|
|
|
|
namespace MiAssessment.Tests.Core;
|
|
|
|
/// <summary>
|
|
/// PaymentRewardDispatcher 属性测试
|
|
/// Feature: framework-template
|
|
///
|
|
/// Property 5: Reward Handler Dispatch
|
|
/// *For any* payment order with a specific order_type:
|
|
/// - The system SHALL search for a registered IPaymentRewardHandler with matching OrderType
|
|
/// - If a handler is found, its ProcessRewardAsync method SHALL be called with the order
|
|
/// - The handler's RewardResult SHALL determine the order's reward_status and reward_data
|
|
///
|
|
/// **Validates: Requirements 6.2, 6.3**
|
|
/// </summary>
|
|
public class PaymentRewardDispatcherPropertyTests
|
|
{
|
|
private readonly Mock<ILogger<PaymentRewardDispatcher>> _mockLogger = new();
|
|
|
|
#region Property 5: Reward Handler Dispatch
|
|
|
|
/// <summary>
|
|
/// Feature: framework-template, Property 5: Reward Handler Dispatch
|
|
///
|
|
/// Property 5.1: Handler found for order type → handler's ProcessRewardAsync is called
|
|
/// If a handler is found, its ProcessRewardAsync method SHALL be called with the order
|
|
///
|
|
/// **Validates: Requirements 6.2, 6.3**
|
|
/// </summary>
|
|
[Property(MaxTest = 100)]
|
|
public bool ProcessReward_WithMatchingHandler_ShouldCallProcessRewardAsync(PositiveInt seed)
|
|
{
|
|
// Arrange
|
|
var orderType = GenerateValidOrderType(seed.Get);
|
|
var handlerCalled = false;
|
|
PaymentOrder? receivedOrder = null;
|
|
|
|
var mockHandler = new Mock<IPaymentRewardHandler>();
|
|
mockHandler.Setup(h => h.OrderType).Returns(orderType);
|
|
mockHandler.Setup(h => h.ProcessRewardAsync(It.IsAny<PaymentOrder>()))
|
|
.Callback<PaymentOrder>(order =>
|
|
{
|
|
handlerCalled = true;
|
|
receivedOrder = order;
|
|
})
|
|
.ReturnsAsync(RewardResult.Ok());
|
|
|
|
var dispatcher = CreateDispatcher(new[] { mockHandler.Object });
|
|
var paymentOrder = CreatePaymentOrder(seed.Get, orderType);
|
|
|
|
// Act
|
|
dispatcher.ProcessRewardAsync(paymentOrder).GetAwaiter().GetResult();
|
|
|
|
// Assert: Handler should be called with the correct order
|
|
return handlerCalled && receivedOrder != null && receivedOrder.OrderNo == paymentOrder.OrderNo;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Feature: framework-template, Property 5: Reward Handler Dispatch
|
|
///
|
|
/// Property 5.2: Handler not found → returns success with no reward data
|
|
/// If no handler is found for the order type, the system should return success (not failure)
|
|
///
|
|
/// **Validates: Requirements 6.2**
|
|
/// </summary>
|
|
[Property(MaxTest = 100)]
|
|
public bool ProcessReward_WithNoMatchingHandler_ShouldReturnSuccessWithNoRewardData(PositiveInt seed)
|
|
{
|
|
// Arrange
|
|
var orderType = GenerateValidOrderType(seed.Get);
|
|
var differentOrderType = $"different_{orderType}";
|
|
|
|
// Create a handler for a different order type
|
|
var mockHandler = new Mock<IPaymentRewardHandler>();
|
|
mockHandler.Setup(h => h.OrderType).Returns(differentOrderType);
|
|
mockHandler.Setup(h => h.ProcessRewardAsync(It.IsAny<PaymentOrder>()))
|
|
.ReturnsAsync(RewardResult.Ok("should_not_be_called"));
|
|
|
|
var dispatcher = CreateDispatcher(new[] { mockHandler.Object });
|
|
var paymentOrder = CreatePaymentOrder(seed.Get, orderType);
|
|
|
|
// Act
|
|
var result = dispatcher.ProcessRewardAsync(paymentOrder).GetAwaiter().GetResult();
|
|
|
|
// Assert: Should return success with no reward data
|
|
return result.Success && result.RewardData == null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Feature: framework-template, Property 5: Reward Handler Dispatch
|
|
///
|
|
/// Property 5.3: Handler returns success → result contains reward data
|
|
/// The handler's RewardResult SHALL determine the order's reward_data
|
|
///
|
|
/// **Validates: Requirements 6.3**
|
|
/// </summary>
|
|
[Property(MaxTest = 100)]
|
|
public bool ProcessReward_WithSuccessfulHandler_ShouldReturnRewardData(PositiveInt seed)
|
|
{
|
|
// Arrange
|
|
var orderType = GenerateValidOrderType(seed.Get);
|
|
var expectedRewardData = $"{{\"diamonds\":{seed.Get % 1000 + 100},\"bonus\":{seed.Get % 50}}}";
|
|
|
|
var mockHandler = new Mock<IPaymentRewardHandler>();
|
|
mockHandler.Setup(h => h.OrderType).Returns(orderType);
|
|
mockHandler.Setup(h => h.ProcessRewardAsync(It.IsAny<PaymentOrder>()))
|
|
.ReturnsAsync(RewardResult.Ok(expectedRewardData));
|
|
|
|
var dispatcher = CreateDispatcher(new[] { mockHandler.Object });
|
|
var paymentOrder = CreatePaymentOrder(seed.Get, orderType);
|
|
|
|
// Act
|
|
var result = dispatcher.ProcessRewardAsync(paymentOrder).GetAwaiter().GetResult();
|
|
|
|
// Assert: Result should contain the reward data from handler
|
|
return result.Success && result.RewardData == expectedRewardData;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Feature: framework-template, Property 5: Reward Handler Dispatch
|
|
///
|
|
/// Property 5.4: Handler returns failure → result contains error message
|
|
/// The handler's RewardResult SHALL determine the order's reward_status
|
|
///
|
|
/// **Validates: Requirements 6.3**
|
|
/// </summary>
|
|
[Property(MaxTest = 100)]
|
|
public bool ProcessReward_WithFailedHandler_ShouldReturnErrorMessage(PositiveInt seed)
|
|
{
|
|
// Arrange
|
|
var orderType = GenerateValidOrderType(seed.Get);
|
|
var expectedErrorMessage = $"Reward processing failed for seed {seed.Get}";
|
|
|
|
var mockHandler = new Mock<IPaymentRewardHandler>();
|
|
mockHandler.Setup(h => h.OrderType).Returns(orderType);
|
|
mockHandler.Setup(h => h.ProcessRewardAsync(It.IsAny<PaymentOrder>()))
|
|
.ReturnsAsync(RewardResult.Fail(expectedErrorMessage));
|
|
|
|
var dispatcher = CreateDispatcher(new[] { mockHandler.Object });
|
|
var paymentOrder = CreatePaymentOrder(seed.Get, orderType);
|
|
|
|
// Act
|
|
var result = dispatcher.ProcessRewardAsync(paymentOrder).GetAwaiter().GetResult();
|
|
|
|
// Assert: Result should contain the error message from handler
|
|
return !result.Success && result.Message == expectedErrorMessage;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Feature: framework-template, Property 5: Reward Handler Dispatch
|
|
///
|
|
/// Property 5.5: Handler throws exception → result contains error message
|
|
/// If the handler throws an exception, the dispatcher should catch it and return failure
|
|
///
|
|
/// **Validates: Requirements 6.3**
|
|
/// </summary>
|
|
[Property(MaxTest = 100)]
|
|
public bool ProcessReward_WithExceptionThrowingHandler_ShouldReturnErrorMessage(PositiveInt seed)
|
|
{
|
|
// Arrange
|
|
var orderType = GenerateValidOrderType(seed.Get);
|
|
var exceptionMessage = $"Unexpected error for seed {seed.Get}";
|
|
|
|
var mockHandler = new Mock<IPaymentRewardHandler>();
|
|
mockHandler.Setup(h => h.OrderType).Returns(orderType);
|
|
mockHandler.Setup(h => h.ProcessRewardAsync(It.IsAny<PaymentOrder>()))
|
|
.ThrowsAsync(new InvalidOperationException(exceptionMessage));
|
|
|
|
var dispatcher = CreateDispatcher(new[] { mockHandler.Object });
|
|
var paymentOrder = CreatePaymentOrder(seed.Get, orderType);
|
|
|
|
// Act
|
|
var result = dispatcher.ProcessRewardAsync(paymentOrder).GetAwaiter().GetResult();
|
|
|
|
// Assert: Result should be failure and contain error message
|
|
return !result.Success && result.Message != null && result.Message.Contains(exceptionMessage);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Feature: framework-template, Property 5: Reward Handler Dispatch
|
|
///
|
|
/// Property 5.6: Multiple handlers registered → correct handler is selected
|
|
/// The system SHALL search for a registered IPaymentRewardHandler with matching OrderType
|
|
///
|
|
/// **Validates: Requirements 6.2**
|
|
/// </summary>
|
|
[Property(MaxTest = 100)]
|
|
public bool ProcessReward_WithMultipleHandlers_ShouldSelectCorrectHandler(PositiveInt seed)
|
|
{
|
|
// Arrange
|
|
var targetOrderType = GenerateValidOrderType(seed.Get);
|
|
var otherOrderType1 = $"other1_{seed.Get}";
|
|
var otherOrderType2 = $"other2_{seed.Get}";
|
|
|
|
var targetRewardData = $"{{\"target_reward\":{seed.Get}}}";
|
|
var targetHandlerCalled = false;
|
|
var otherHandler1Called = false;
|
|
var otherHandler2Called = false;
|
|
|
|
// Create target handler
|
|
var targetHandler = new Mock<IPaymentRewardHandler>();
|
|
targetHandler.Setup(h => h.OrderType).Returns(targetOrderType);
|
|
targetHandler.Setup(h => h.ProcessRewardAsync(It.IsAny<PaymentOrder>()))
|
|
.Callback(() => targetHandlerCalled = true)
|
|
.ReturnsAsync(RewardResult.Ok(targetRewardData));
|
|
|
|
// Create other handlers
|
|
var otherHandler1 = new Mock<IPaymentRewardHandler>();
|
|
otherHandler1.Setup(h => h.OrderType).Returns(otherOrderType1);
|
|
otherHandler1.Setup(h => h.ProcessRewardAsync(It.IsAny<PaymentOrder>()))
|
|
.Callback(() => otherHandler1Called = true)
|
|
.ReturnsAsync(RewardResult.Ok("other1_data"));
|
|
|
|
var otherHandler2 = new Mock<IPaymentRewardHandler>();
|
|
otherHandler2.Setup(h => h.OrderType).Returns(otherOrderType2);
|
|
otherHandler2.Setup(h => h.ProcessRewardAsync(It.IsAny<PaymentOrder>()))
|
|
.Callback(() => otherHandler2Called = true)
|
|
.ReturnsAsync(RewardResult.Ok("other2_data"));
|
|
|
|
var dispatcher = CreateDispatcher(new[]
|
|
{
|
|
otherHandler1.Object,
|
|
targetHandler.Object,
|
|
otherHandler2.Object
|
|
});
|
|
|
|
var paymentOrder = CreatePaymentOrder(seed.Get, targetOrderType);
|
|
|
|
// Act
|
|
var result = dispatcher.ProcessRewardAsync(paymentOrder).GetAwaiter().GetResult();
|
|
|
|
// Assert: Only target handler should be called, result should match target handler's output
|
|
return targetHandlerCalled &&
|
|
!otherHandler1Called &&
|
|
!otherHandler2Called &&
|
|
result.Success &&
|
|
result.RewardData == targetRewardData;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Feature: framework-template, Property 5: Reward Handler Dispatch
|
|
///
|
|
/// Property 5.7: GetHandler returns correct handler for registered order type
|
|
/// The system SHALL search for a registered IPaymentRewardHandler with matching OrderType
|
|
///
|
|
/// **Validates: Requirements 6.2**
|
|
/// </summary>
|
|
[Property(MaxTest = 100)]
|
|
public bool GetHandler_WithRegisteredOrderType_ShouldReturnCorrectHandler(PositiveInt seed)
|
|
{
|
|
// Arrange
|
|
var orderType = GenerateValidOrderType(seed.Get);
|
|
|
|
var mockHandler = new Mock<IPaymentRewardHandler>();
|
|
mockHandler.Setup(h => h.OrderType).Returns(orderType);
|
|
|
|
var dispatcher = CreateDispatcher(new[] { mockHandler.Object });
|
|
|
|
// Act
|
|
var handler = dispatcher.GetHandler(orderType);
|
|
|
|
// Assert: Should return the registered handler
|
|
return handler != null && handler.OrderType == orderType;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Feature: framework-template, Property 5: Reward Handler Dispatch
|
|
///
|
|
/// Property 5.8: GetHandler returns null for unregistered order type
|
|
/// If no handler is found, GetHandler should return null
|
|
///
|
|
/// **Validates: Requirements 6.2**
|
|
/// </summary>
|
|
[Property(MaxTest = 100)]
|
|
public bool GetHandler_WithUnregisteredOrderType_ShouldReturnNull(PositiveInt seed)
|
|
{
|
|
// Arrange
|
|
var registeredOrderType = GenerateValidOrderType(seed.Get);
|
|
var unregisteredOrderType = $"unregistered_{seed.Get}";
|
|
|
|
var mockHandler = new Mock<IPaymentRewardHandler>();
|
|
mockHandler.Setup(h => h.OrderType).Returns(registeredOrderType);
|
|
|
|
var dispatcher = CreateDispatcher(new[] { mockHandler.Object });
|
|
|
|
// Act
|
|
var handler = dispatcher.GetHandler(unregisteredOrderType);
|
|
|
|
// Assert: Should return null for unregistered order type
|
|
return handler == null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Feature: framework-template, Property 5: Reward Handler Dispatch
|
|
///
|
|
/// Property 5.9: HasHandler returns true for registered order type
|
|
/// The system should correctly identify if a handler exists for an order type
|
|
///
|
|
/// **Validates: Requirements 6.2**
|
|
/// </summary>
|
|
[Property(MaxTest = 100)]
|
|
public bool HasHandler_WithRegisteredOrderType_ShouldReturnTrue(PositiveInt seed)
|
|
{
|
|
// Arrange
|
|
var orderType = GenerateValidOrderType(seed.Get);
|
|
|
|
var mockHandler = new Mock<IPaymentRewardHandler>();
|
|
mockHandler.Setup(h => h.OrderType).Returns(orderType);
|
|
|
|
var dispatcher = CreateDispatcher(new[] { mockHandler.Object });
|
|
|
|
// Act
|
|
var hasHandler = dispatcher.HasHandler(orderType);
|
|
|
|
// Assert: Should return true for registered order type
|
|
return hasHandler;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Feature: framework-template, Property 5: Reward Handler Dispatch
|
|
///
|
|
/// Property 5.10: HasHandler returns false for unregistered order type
|
|
/// The system should correctly identify if no handler exists for an order type
|
|
///
|
|
/// **Validates: Requirements 6.2**
|
|
/// </summary>
|
|
[Property(MaxTest = 100)]
|
|
public bool HasHandler_WithUnregisteredOrderType_ShouldReturnFalse(PositiveInt seed)
|
|
{
|
|
// Arrange
|
|
var registeredOrderType = GenerateValidOrderType(seed.Get);
|
|
var unregisteredOrderType = $"unregistered_{seed.Get}";
|
|
|
|
var mockHandler = new Mock<IPaymentRewardHandler>();
|
|
mockHandler.Setup(h => h.OrderType).Returns(registeredOrderType);
|
|
|
|
var dispatcher = CreateDispatcher(new[] { mockHandler.Object });
|
|
|
|
// Act
|
|
var hasHandler = dispatcher.HasHandler(unregisteredOrderType);
|
|
|
|
// Assert: Should return false for unregistered order type
|
|
return !hasHandler;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Feature: framework-template, Property 5: Reward Handler Dispatch
|
|
///
|
|
/// Property 5.11: GetRegisteredOrderTypes returns all registered order types
|
|
/// The system should track all registered handlers
|
|
///
|
|
/// **Validates: Requirements 6.2**
|
|
/// </summary>
|
|
[Property(MaxTest = 100)]
|
|
public bool GetRegisteredOrderTypes_ShouldReturnAllRegisteredTypes(PositiveInt seed)
|
|
{
|
|
// Arrange
|
|
var orderType1 = $"type1_{seed.Get}";
|
|
var orderType2 = $"type2_{seed.Get}";
|
|
var orderType3 = $"type3_{seed.Get}";
|
|
|
|
var handler1 = new Mock<IPaymentRewardHandler>();
|
|
handler1.Setup(h => h.OrderType).Returns(orderType1);
|
|
|
|
var handler2 = new Mock<IPaymentRewardHandler>();
|
|
handler2.Setup(h => h.OrderType).Returns(orderType2);
|
|
|
|
var handler3 = new Mock<IPaymentRewardHandler>();
|
|
handler3.Setup(h => h.OrderType).Returns(orderType3);
|
|
|
|
var dispatcher = CreateDispatcher(new[]
|
|
{
|
|
handler1.Object,
|
|
handler2.Object,
|
|
handler3.Object
|
|
});
|
|
|
|
// Act
|
|
var registeredTypes = dispatcher.GetRegisteredOrderTypes();
|
|
|
|
// Assert: Should contain all registered order types
|
|
return registeredTypes.Count == 3 &&
|
|
registeredTypes.Contains(orderType1) &&
|
|
registeredTypes.Contains(orderType2) &&
|
|
registeredTypes.Contains(orderType3);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Feature: framework-template, Property 5: Reward Handler Dispatch
|
|
///
|
|
/// Property 5.12: ProcessReward with null order returns failure
|
|
/// The dispatcher should handle null orders gracefully
|
|
///
|
|
/// **Validates: Requirements 6.3**
|
|
/// </summary>
|
|
[Fact]
|
|
public void ProcessReward_WithNullOrder_ShouldReturnFailure()
|
|
{
|
|
// Arrange
|
|
var dispatcher = CreateDispatcher(Array.Empty<IPaymentRewardHandler>());
|
|
|
|
// Act
|
|
var result = dispatcher.ProcessRewardAsync(null!).GetAwaiter().GetResult();
|
|
|
|
// Assert: Should return failure
|
|
Assert.False(result.Success);
|
|
Assert.NotNull(result.Message);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Feature: framework-template, Property 5: Reward Handler Dispatch
|
|
///
|
|
/// Property 5.13: ProcessReward with empty order type returns failure
|
|
/// The dispatcher should handle orders with empty order type gracefully
|
|
///
|
|
/// **Validates: Requirements 6.3**
|
|
/// </summary>
|
|
[Property(MaxTest = 100)]
|
|
public bool ProcessReward_WithEmptyOrderType_ShouldReturnFailure(PositiveInt seed)
|
|
{
|
|
// Arrange
|
|
var dispatcher = CreateDispatcher(Array.Empty<IPaymentRewardHandler>());
|
|
var paymentOrder = CreatePaymentOrder(seed.Get, "");
|
|
|
|
// Act
|
|
var result = dispatcher.ProcessRewardAsync(paymentOrder).GetAwaiter().GetResult();
|
|
|
|
// Assert: Should return failure
|
|
return !result.Success && result.Message != null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Feature: framework-template, Property 5: Reward Handler Dispatch
|
|
///
|
|
/// Property 5.14: Order type matching is case-insensitive
|
|
/// The system should match order types regardless of case
|
|
///
|
|
/// **Validates: Requirements 6.2**
|
|
/// </summary>
|
|
[Property(MaxTest = 100)]
|
|
public bool ProcessReward_OrderTypeMatching_ShouldBeCaseInsensitive(PositiveInt seed)
|
|
{
|
|
// Arrange
|
|
var lowerCaseOrderType = $"order_type_{seed.Get}".ToLowerInvariant();
|
|
var upperCaseOrderType = lowerCaseOrderType.ToUpperInvariant();
|
|
var expectedRewardData = $"{{\"reward\":{seed.Get}}}";
|
|
|
|
var mockHandler = new Mock<IPaymentRewardHandler>();
|
|
mockHandler.Setup(h => h.OrderType).Returns(lowerCaseOrderType);
|
|
mockHandler.Setup(h => h.ProcessRewardAsync(It.IsAny<PaymentOrder>()))
|
|
.ReturnsAsync(RewardResult.Ok(expectedRewardData));
|
|
|
|
var dispatcher = CreateDispatcher(new[] { mockHandler.Object });
|
|
var paymentOrder = CreatePaymentOrder(seed.Get, upperCaseOrderType);
|
|
|
|
// Act
|
|
var result = dispatcher.ProcessRewardAsync(paymentOrder).GetAwaiter().GetResult();
|
|
|
|
// Assert: Should find handler despite case difference
|
|
return result.Success && result.RewardData == expectedRewardData;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Feature: framework-template, Property 5: Reward Handler Dispatch
|
|
///
|
|
/// Property 5.15: Empty handlers collection returns success for any order
|
|
/// When no handlers are registered, processing should succeed with no reward
|
|
///
|
|
/// **Validates: Requirements 6.2**
|
|
/// </summary>
|
|
[Property(MaxTest = 100)]
|
|
public bool ProcessReward_WithNoHandlers_ShouldReturnSuccess(PositiveInt seed)
|
|
{
|
|
// Arrange
|
|
var dispatcher = CreateDispatcher(Array.Empty<IPaymentRewardHandler>());
|
|
var paymentOrder = CreatePaymentOrder(seed.Get, GenerateValidOrderType(seed.Get));
|
|
|
|
// Act
|
|
var result = dispatcher.ProcessRewardAsync(paymentOrder).GetAwaiter().GetResult();
|
|
|
|
// Assert: Should return success with no reward data
|
|
return result.Success && result.RewardData == null;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Helper Methods
|
|
|
|
/// <summary>
|
|
/// 创建 PaymentRewardDispatcher 实例
|
|
/// </summary>
|
|
private PaymentRewardDispatcher CreateDispatcher(IEnumerable<IPaymentRewardHandler> handlers)
|
|
{
|
|
return new PaymentRewardDispatcher(handlers, _mockLogger.Object);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 创建测试用的 PaymentOrder
|
|
/// </summary>
|
|
private PaymentOrder CreatePaymentOrder(int seed, string orderType)
|
|
{
|
|
return new PaymentOrder
|
|
{
|
|
Id = seed,
|
|
OrderNo = $"PO{DateTime.Now:yyyyMMddHHmmss}{seed:D6}",
|
|
UserId = Math.Max(1, seed % 10000),
|
|
OrderType = orderType,
|
|
Title = $"测试订单 {seed}",
|
|
Amount = Math.Round((decimal)((seed % 10000) + 100) / 100, 2),
|
|
PayAmount = Math.Round((decimal)((seed % 10000) + 100) / 100, 2),
|
|
PayMethod = "wechat",
|
|
Status = 1, // 已支付
|
|
PaidAt = DateTime.Now,
|
|
TransactionId = $"TX_{seed}",
|
|
BizData = $"{{\"seed\":{seed}}}",
|
|
RewardStatus = 0,
|
|
CreatedAt = DateTime.Now,
|
|
UpdatedAt = DateTime.Now
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// 生成有效的订单类型
|
|
/// </summary>
|
|
private string GenerateValidOrderType(int seed)
|
|
{
|
|
var types = new[] { "diamond_recharge", "vip_purchase", "gift_buy", "subscription", "premium" };
|
|
return $"{types[seed % types.Length]}_{seed % 1000}";
|
|
}
|
|
|
|
#endregion
|
|
}
|