feat(api): .NET 10 multi-tenant REST API
Full backend implementation: - Multi-tenant cafe/restaurant management (menus, orders, tables, staff) - POS order flow with ZarinPal and Snappfood payment integration - OTP authentication via Kavenegar SMS - QR digital menu with public discover/finder endpoints - Customer loyalty, coupons, CRM - PostgreSQL via EF Core, Redis for caching/sessions - Background jobs, webhook handlers - Full migration history Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
using Meezi.API.Configuration;
|
||||
using Meezi.API.Models.Snappfood;
|
||||
using Meezi.API.Models.Tap30;
|
||||
using Meezi.API.Services.Delivery;
|
||||
using Meezi.Core.Delivery;
|
||||
using Meezi.Core.Enums;
|
||||
using Meezi.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Xunit;
|
||||
|
||||
namespace Meezi.API.Tests;
|
||||
|
||||
public class DeliveryIntegrationTests
|
||||
{
|
||||
private static DeliveryPlatformsOptions TestOptions() => new()
|
||||
{
|
||||
DefaultSnappfoodCommissionPercent = 18,
|
||||
DefaultTap30CommissionPercent = 15
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void OrderNormalizer_Snappfood_MapsToUnifiedOrder()
|
||||
{
|
||||
var normalizer = new OrderNormalizer();
|
||||
var unified = normalizer.FromSnappfood(new SnappfoodWebhookOrder(
|
||||
"sf-100",
|
||||
"vendor-1",
|
||||
"Ali",
|
||||
"09121234567",
|
||||
250_000m,
|
||||
[new SnappfoodWebhookItem("Espresso", 2, 100_000m)]));
|
||||
|
||||
Assert.NotNull(unified);
|
||||
Assert.Equal("sf-100", unified!.ExternalId);
|
||||
Assert.Equal(DeliveryPlatform.Snappfood, unified.Platform);
|
||||
Assert.Equal(2, unified.Items[0].Quantity);
|
||||
Assert.Equal(UnifiedDeliveryStatus.Confirmed, unified.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OrderNormalizer_Tap30_MapsToUnifiedOrder()
|
||||
{
|
||||
var normalizer = new OrderNormalizer();
|
||||
var unified = normalizer.FromTap30(new Tap30WebhookOrder(
|
||||
"t30-55",
|
||||
"tap-vendor",
|
||||
new Tap30Customer("Sara", "09129876543", "Tehran", null, null),
|
||||
180_000m,
|
||||
"online",
|
||||
true,
|
||||
null,
|
||||
"delivery",
|
||||
35,
|
||||
null,
|
||||
null,
|
||||
"confirmed",
|
||||
[new Tap30WebhookItem("latte-1", "Latte", 1, 180_000m, null)]));
|
||||
|
||||
Assert.NotNull(unified);
|
||||
Assert.Equal(DeliveryPlatform.Tap30, unified!.Platform);
|
||||
Assert.Equal("Sara", unified.Customer.Name);
|
||||
Assert.Equal(35, unified.Delivery.EstimatedMinutes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CommissionCalculator_SnappfoodDefault_Is18Percent()
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<AppDbContext>()
|
||||
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
||||
.Options;
|
||||
await using var db = new AppDbContext(options);
|
||||
var calc = new CommissionCalculator(db, Options.Create(TestOptions()));
|
||||
|
||||
var commission = await calc.CalculateForOrderAsync(
|
||||
"cafe-1",
|
||||
new UnifiedDeliveryOrder(
|
||||
"x1",
|
||||
DeliveryPlatform.Snappfood,
|
||||
"v1",
|
||||
DateTime.UtcNow,
|
||||
new UnifiedDeliveryCustomer("A", "09"),
|
||||
[new UnifiedDeliveryItem("1", "Coffee", 1, 100_000m)],
|
||||
new UnifiedDeliveryPayment(100_000m, "online", true),
|
||||
new UnifiedDeliveryInfo("delivery"),
|
||||
UnifiedDeliveryStatus.Confirmed));
|
||||
|
||||
Assert.Equal(18_000m, commission);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CommissionCalculator_Tap30Default_Is15Percent()
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<AppDbContext>()
|
||||
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
||||
.Options;
|
||||
await using var db = new AppDbContext(options);
|
||||
var calc = new CommissionCalculator(db, Options.Create(TestOptions()));
|
||||
|
||||
var rate = await calc.ResolveRatePercentAsync("cafe-1", DeliveryPlatform.Tap30);
|
||||
Assert.Equal(15m, rate);
|
||||
Assert.Equal(15_000m, calc.CalculateCommission(100_000m, rate));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WebhookSignature_ValidHmac_Accepts()
|
||||
{
|
||||
const string secret = "test-secret";
|
||||
const string body = """{"orderId":"1"}""";
|
||||
var expected = WebhookSignatureService.ComputeHmacSha256Hex(body, secret);
|
||||
|
||||
var svc = new WebhookSignatureService(Options.Create(new DeliveryPlatformsOptions
|
||||
{
|
||||
Snappfood = new SnappfoodPlatformOptions { WebhookSecret = secret }
|
||||
}));
|
||||
|
||||
Assert.True(svc.Verify(DeliveryPlatform.Snappfood, body, expected));
|
||||
Assert.True(svc.Verify(DeliveryPlatform.Snappfood, body, $"sha256={expected}"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user