8ce0b3e3e8
CI/CD / CI · API (dotnet build + test) (push) Successful in 40s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 32s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m4s
CI/CD / CI · Admin Web (tsc) (push) Successful in 35s
CI/CD / CI · Website (tsc) (push) Successful in 45s
CI/CD / CI · Koja (tsc) (push) Successful in 48s
CI/CD / Deploy · all services (push) Successful in 4m11s
Showcase cafés (dev/staging only) now get Latitude/Longitude scattered around their real city (Tehran/Karaj) with a deterministic per-id offset, so the homepage Iran map renders a realistic cluster of blinking merchant lights. Backfills existing rows where coords are null. Production cafés get coordinates when owners set their location in dashboard Settings. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
271 lines
9.7 KiB
C#
271 lines
9.7 KiB
C#
using Meezi.Core.Branding;
|
|
using Meezi.Core.Entities;
|
|
using Meezi.Core.Enums;
|
|
using Meezi.Infrastructure.Branding;
|
|
using Meezi.Infrastructure.Discover;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace Meezi.Infrastructure.Data;
|
|
|
|
/// <summary>Seeds 30 Persian showcase cafés for public discover (development only).</summary>
|
|
public static class DiscoverShowcaseSeeder
|
|
{
|
|
// Approximate city centres. Each café is scattered around its city with a
|
|
// small deterministic offset (derived from its id) so the marketing map
|
|
// shows a realistic cluster of blinking lights instead of one stacked dot.
|
|
private static readonly Dictionary<string, (double Lat, double Lng, double Spread)> CityGeo = new()
|
|
{
|
|
["تهران"] = (35.70, 51.39, 0.13),
|
|
["کرج"] = (35.83, 50.99, 0.07),
|
|
};
|
|
|
|
private static (double Lat, double Lng) GeoFor(string id, string city)
|
|
{
|
|
var (lat, lng, spread) = CityGeo.TryGetValue(city, out var g) ? g : (35.70, 51.39, 0.13);
|
|
unchecked
|
|
{
|
|
var h = 17;
|
|
foreach (var ch in id) h = (h * 31) + ch;
|
|
var ox = (((h & 0xFFFF) / 65535.0) - 0.5) * 2 * spread;
|
|
var oy = ((((h >> 16) & 0xFFFF) / 65535.0) - 0.5) * 2 * spread;
|
|
return (Math.Round(lat + oy, 5), Math.Round(lng + ox, 5));
|
|
}
|
|
}
|
|
|
|
private static readonly string[] ReviewAuthors = ["سارا", "علی", "مینا", "رضا", "نازنین"];
|
|
private static readonly string[] ReviewComments =
|
|
[
|
|
"فضا و نوشیدنی عالی بود.",
|
|
"سرویس سریع، پیشنهاد میکنم.",
|
|
"مناسب قرار و کار.",
|
|
"قیمت مناسب برای کیفیت.",
|
|
"دسر و قهوه خوشمزه بود.",
|
|
];
|
|
|
|
public static async Task SeedAsync(AppDbContext db, ILogger logger)
|
|
{
|
|
var addedCafes = 0;
|
|
foreach (var spec in DiscoverShowcaseCatalog.Cafes)
|
|
{
|
|
var cafe = await db.Cafes.FirstOrDefaultAsync(c => c.Id == spec.Id);
|
|
var (geoLat, geoLng) = GeoFor(spec.Id, spec.City);
|
|
if (cafe is null)
|
|
{
|
|
cafe = new Cafe
|
|
{
|
|
Id = spec.Id,
|
|
Name = spec.Name,
|
|
NameEn = spec.Slug.Replace('-', ' '),
|
|
Slug = spec.Slug,
|
|
City = spec.City,
|
|
Address = spec.Address,
|
|
Latitude = geoLat,
|
|
Longitude = geoLng,
|
|
Description = spec.Description,
|
|
PlanTier = spec.PlanTier,
|
|
PreferredLanguage = "fa",
|
|
IsVerified = true,
|
|
DiscoverProfileJson = CafeDiscoverProfileSerializer.Serialize(spec.Profile),
|
|
DiscoverBadgesJson = DiscoverBadgesSerializer.Serialize(spec.Badges),
|
|
ThemeJson = CafeThemeSerializer.Serialize(new CafeTheme
|
|
{
|
|
PaletteId = CafeThemeDefaults.PaletteMeeziGreen,
|
|
PanelStyle = CafeThemeDefaults.PanelModern,
|
|
MenuStyle = CafeThemeDefaults.MenuCards,
|
|
Density = CafeThemeDefaults.DensityComfortable,
|
|
Radius = CafeThemeDefaults.RadiusMd
|
|
})
|
|
};
|
|
db.Cafes.Add(cafe);
|
|
|
|
var branchId = BranchId(spec.Id);
|
|
db.Branches.Add(new Branch
|
|
{
|
|
Id = branchId,
|
|
CafeId = spec.Id,
|
|
Name = "شعبه اصلی",
|
|
City = spec.City,
|
|
Address = spec.Address,
|
|
Phone = "02100000000",
|
|
IsActive = true,
|
|
CreatedAt = DateTime.UtcNow,
|
|
UpdatedAt = DateTime.UtcNow
|
|
});
|
|
|
|
db.Employees.Add(new Employee
|
|
{
|
|
Id = OwnerId(spec.Id),
|
|
CafeId = spec.Id,
|
|
BranchId = branchId,
|
|
Name = $"مدیر {spec.Name}",
|
|
Phone = spec.OwnerPhone,
|
|
Role = EmployeeRole.Owner,
|
|
BaseSalary = 0
|
|
});
|
|
|
|
await db.SaveChangesAsync();
|
|
addedCafes++;
|
|
}
|
|
else
|
|
{
|
|
var changed = false;
|
|
if (string.IsNullOrEmpty(cafe.DiscoverProfileJson))
|
|
{
|
|
cafe.DiscoverProfileJson = CafeDiscoverProfileSerializer.Serialize(spec.Profile);
|
|
changed = true;
|
|
}
|
|
if (string.IsNullOrEmpty(cafe.DiscoverBadgesJson) && spec.Badges is { Count: > 0 })
|
|
{
|
|
cafe.DiscoverBadgesJson = DiscoverBadgesSerializer.Serialize(spec.Badges);
|
|
changed = true;
|
|
}
|
|
if (!cafe.IsVerified)
|
|
{
|
|
cafe.IsVerified = true;
|
|
changed = true;
|
|
}
|
|
if (cafe.Latitude is null || cafe.Longitude is null)
|
|
{
|
|
cafe.Latitude = geoLat;
|
|
cafe.Longitude = geoLng;
|
|
changed = true;
|
|
}
|
|
if (changed)
|
|
await db.SaveChangesAsync();
|
|
}
|
|
|
|
await EnsureShowcaseMenuAsync(db, spec, logger);
|
|
await EnsureShowcaseReviewsAsync(db, spec.Id, logger);
|
|
}
|
|
|
|
if (addedCafes > 0)
|
|
logger.LogInformation("Discover showcase seed: {Count} new cafés", addedCafes);
|
|
}
|
|
|
|
private static string BranchId(string cafeId) => $"branch_{cafeId}_main";
|
|
private static string OwnerId(string cafeId) => $"emp_{cafeId}_owner";
|
|
|
|
private static async Task EnsureShowcaseMenuAsync(
|
|
AppDbContext db,
|
|
DiscoverShowcaseCatalog.ShowcaseCafe spec,
|
|
ILogger logger)
|
|
{
|
|
var taxId = $"tax_{spec.Id}";
|
|
if (!await db.Taxes.AnyAsync(t => t.Id == taxId))
|
|
{
|
|
db.Taxes.Add(new Tax
|
|
{
|
|
Id = taxId,
|
|
CafeId = spec.Id,
|
|
Name = "مالیات",
|
|
Rate = 9,
|
|
IsDefault = true,
|
|
IsRequired = true,
|
|
IsCompound = false
|
|
});
|
|
await db.SaveChangesAsync();
|
|
}
|
|
|
|
var templateIndex = spec.MenuTemplateIndex % DiscoverShowcaseMenus.Templates.Count;
|
|
var template = DiscoverShowcaseMenus.Templates[templateIndex];
|
|
|
|
var existingCats = await db.MenuCategories
|
|
.Where(c => c.CafeId == spec.Id)
|
|
.Select(c => c.Id)
|
|
.ToListAsync();
|
|
|
|
var catsAdded = 0;
|
|
foreach (var cat in template.Categories)
|
|
{
|
|
var catId = Prefixed(spec.Id, cat.Id);
|
|
if (existingCats.Contains(catId))
|
|
continue;
|
|
|
|
db.MenuCategories.Add(new MenuCategory
|
|
{
|
|
Id = catId,
|
|
CafeId = spec.Id,
|
|
Name = cat.Name,
|
|
NameEn = cat.NameEn,
|
|
NameAr = cat.NameAr,
|
|
Icon = cat.Icon,
|
|
IconPresetId = cat.IconPresetId,
|
|
IconStyle = cat.IconStyle,
|
|
SortOrder = cat.SortOrder,
|
|
TaxId = taxId,
|
|
IsActive = true
|
|
});
|
|
catsAdded++;
|
|
}
|
|
|
|
if (catsAdded > 0)
|
|
await db.SaveChangesAsync();
|
|
|
|
var existingItems = await db.MenuItems
|
|
.Where(i => i.CafeId == spec.Id)
|
|
.Select(i => i.Id)
|
|
.ToListAsync();
|
|
|
|
var itemsAdded = 0;
|
|
foreach (var item in template.Items)
|
|
{
|
|
var itemId = Prefixed(spec.Id, item.Id);
|
|
if (existingItems.Contains(itemId))
|
|
continue;
|
|
|
|
var catId = Prefixed(spec.Id, item.CategoryId);
|
|
db.MenuItems.Add(new MenuItem
|
|
{
|
|
Id = itemId,
|
|
CafeId = spec.Id,
|
|
CategoryId = catId,
|
|
Name = item.Name,
|
|
NameEn = item.NameEn,
|
|
NameAr = item.NameAr,
|
|
Description = item.Description,
|
|
Price = item.PriceToman,
|
|
DiscountPercent = item.DiscountPercent,
|
|
ImageUrl = DemoMenuCatalog.ResolveItemImageUrl(item),
|
|
IsAvailable = true
|
|
});
|
|
itemsAdded++;
|
|
}
|
|
|
|
if (itemsAdded > 0)
|
|
{
|
|
await db.SaveChangesAsync();
|
|
logger.LogInformation(
|
|
"Showcase menu: cafe {Slug} +{Cats} cats +{Items} items",
|
|
spec.Slug,
|
|
catsAdded,
|
|
itemsAdded);
|
|
}
|
|
}
|
|
|
|
private static async Task EnsureShowcaseReviewsAsync(AppDbContext db, string cafeId, ILogger logger)
|
|
{
|
|
if (await db.CafeReviews.CountAsync(r => r.CafeId == cafeId) >= 2)
|
|
return;
|
|
|
|
var rng = new Random(cafeId.GetHashCode(StringComparison.Ordinal));
|
|
var count = 2 - await db.CafeReviews.CountAsync(r => r.CafeId == cafeId);
|
|
for (var i = 0; i < count; i++)
|
|
{
|
|
db.CafeReviews.Add(new CafeReview
|
|
{
|
|
CafeId = cafeId,
|
|
AuthorName = ReviewAuthors[rng.Next(ReviewAuthors.Length)],
|
|
Rating = rng.Next(4, 6),
|
|
Comment = ReviewComments[rng.Next(ReviewComments.Length)],
|
|
CreatedAt = DateTime.UtcNow.AddDays(-rng.Next(1, 30))
|
|
});
|
|
}
|
|
|
|
await db.SaveChangesAsync();
|
|
logger.LogDebug("Showcase reviews seeded for {CafeId}", cafeId);
|
|
}
|
|
|
|
private static string Prefixed(string cafeId, string seedId) => $"{cafeId}_{seedId}";
|
|
}
|