feat(plans): menu watermark on Free (removed by paid feature)
Guest QR menu shows a "ساختهشده با میزی" watermark under the menu unless the café's
plan has the `watermark_removed` feature (Starter+).
- PublicMenuDto gains ShowWatermark; PublicService computes it from
IsFeatureEnabledForCafeAsync("watermark_removed") for both slug and branch menus.
- Guest menu renders the watermark footer when showWatermark.
- NoOpPlatformCatalogService test double (all features on) for the PublicService
ctor; QrMenuTests updated.
86 tests pass; dashboard tsc clean.
This commit is contained in:
@@ -107,7 +107,8 @@ public record PublicMenuDto(
|
|||||||
string CafeName,
|
string CafeName,
|
||||||
string Slug,
|
string Slug,
|
||||||
CafeThemeDto Theme,
|
CafeThemeDto Theme,
|
||||||
IReadOnlyList<PublicMenuCategoryDto> Categories);
|
IReadOnlyList<PublicMenuCategoryDto> Categories,
|
||||||
|
bool ShowWatermark);
|
||||||
|
|
||||||
public record GuestCreateOrderRequest(
|
public record GuestCreateOrderRequest(
|
||||||
OrderType OrderType,
|
OrderType OrderType,
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ public class PublicService : IPublicService
|
|||||||
private readonly IBranchIdentityService _identity;
|
private readonly IBranchIdentityService _identity;
|
||||||
private readonly IAbuseProtectionService _abuse;
|
private readonly IAbuseProtectionService _abuse;
|
||||||
private readonly IHttpContextAccessor _http;
|
private readonly IHttpContextAccessor _http;
|
||||||
|
private readonly Meezi.Infrastructure.Services.Platform.IPlatformCatalogService _catalog;
|
||||||
|
|
||||||
public PublicService(
|
public PublicService(
|
||||||
AppDbContext db,
|
AppDbContext db,
|
||||||
@@ -62,7 +63,8 @@ public class PublicService : IPublicService
|
|||||||
IBranchMenuService branchMenu,
|
IBranchMenuService branchMenu,
|
||||||
IBranchIdentityService identity,
|
IBranchIdentityService identity,
|
||||||
IAbuseProtectionService abuse,
|
IAbuseProtectionService abuse,
|
||||||
IHttpContextAccessor http)
|
IHttpContextAccessor http,
|
||||||
|
Meezi.Infrastructure.Services.Platform.IPlatformCatalogService catalog)
|
||||||
{
|
{
|
||||||
_db = db;
|
_db = db;
|
||||||
_orders = orders;
|
_orders = orders;
|
||||||
@@ -72,8 +74,13 @@ public class PublicService : IPublicService
|
|||||||
_identity = identity;
|
_identity = identity;
|
||||||
_abuse = abuse;
|
_abuse = abuse;
|
||||||
_http = http;
|
_http = http;
|
||||||
|
_catalog = catalog;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Free menus show a Meezi watermark; the `watermark_removed` feature (paid) hides it.</summary>
|
||||||
|
private async Task<bool> ShowWatermarkAsync(Cafe cafe, CancellationToken ct) =>
|
||||||
|
!await _catalog.IsFeatureEnabledForCafeAsync(cafe.Id, cafe.PlanTier, "watermark_removed", ct);
|
||||||
|
|
||||||
public Task<IReadOnlyList<CafeDiscoverDto>> DiscoverAsync(
|
public Task<IReadOnlyList<CafeDiscoverDto>> DiscoverAsync(
|
||||||
DiscoverFilterParams filters,
|
DiscoverFilterParams filters,
|
||||||
CancellationToken cancellationToken = default) =>
|
CancellationToken cancellationToken = default) =>
|
||||||
@@ -190,7 +197,8 @@ public class PublicService : IPublicService
|
|||||||
.Where(c => c.Items.Count > 0)
|
.Where(c => c.Items.Count > 0)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
return new PublicMenuDto(cafe.Id, cafe.Name, cafe.Slug, CafeThemeMapping.FromJson(cafe.ThemeJson), grouped);
|
return new PublicMenuDto(cafe.Id, cafe.Name, cafe.Slug, CafeThemeMapping.FromJson(cafe.ThemeJson), grouped,
|
||||||
|
await ShowWatermarkAsync(cafe, cancellationToken));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<(GuestOrderPlacedDto? Data, string? ErrorCode, string? ErrorMessage)> PlaceOrderAsync(
|
public async Task<(GuestOrderPlacedDto? Data, string? ErrorCode, string? ErrorMessage)> PlaceOrderAsync(
|
||||||
@@ -357,7 +365,8 @@ public class PublicService : IPublicService
|
|||||||
.OrderBy(c => categoryById.GetValueOrDefault(c.Id)?.SortOrder ?? 0)
|
.OrderBy(c => categoryById.GetValueOrDefault(c.Id)?.SortOrder ?? 0)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
return new PublicMenuDto(cafe.Id, cafe.Name, cafe.Slug, CafeThemeMapping.FromJson(cafe.ThemeJson), grouped);
|
return new PublicMenuDto(cafe.Id, cafe.Name, cafe.Slug, CafeThemeMapping.FromJson(cafe.ThemeJson), grouped,
|
||||||
|
await ShowWatermarkAsync(cafe, cancellationToken));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<(GuestQrOrderPlacedDto? Data, string? ErrorCode, string? Message)> PlaceBranchGuestOrderAsync(
|
public async Task<(GuestQrOrderPlacedDto? Data, string? ErrorCode, string? Message)> PlaceBranchGuestOrderAsync(
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
using Meezi.Core.Enums;
|
||||||
|
using Meezi.Core.Platform;
|
||||||
|
using Meezi.Infrastructure.Services.Platform;
|
||||||
|
|
||||||
|
namespace Meezi.API.Tests;
|
||||||
|
|
||||||
|
/// <summary>Test double: every feature enabled, unlimited limits. Keeps plan gating
|
||||||
|
/// out of the way for service-level tests.</summary>
|
||||||
|
internal sealed class NoOpPlatformCatalogService : IPlatformCatalogService
|
||||||
|
{
|
||||||
|
public Task<IReadOnlyList<PlanDefinitionDto>> GetPlansAsync(CancellationToken ct = default) =>
|
||||||
|
Task.FromResult<IReadOnlyList<PlanDefinitionDto>>([]);
|
||||||
|
|
||||||
|
public Task<PlanDefinitionDto?> GetPlanAsync(PlanTier tier, CancellationToken ct = default) =>
|
||||||
|
Task.FromResult<PlanDefinitionDto?>(null);
|
||||||
|
|
||||||
|
public Task<PlanLimitsData> GetLimitsAsync(PlanTier tier, CancellationToken ct = default) =>
|
||||||
|
Task.FromResult(new PlanLimitsData());
|
||||||
|
|
||||||
|
public Task<decimal> GetMonthlyPriceTomanAsync(PlanTier tier, CancellationToken ct = default) =>
|
||||||
|
Task.FromResult(0m);
|
||||||
|
|
||||||
|
public Task<bool> IsBillableOnlineAsync(PlanTier tier, CancellationToken ct = default) =>
|
||||||
|
Task.FromResult(false);
|
||||||
|
|
||||||
|
public Task<IReadOnlyList<PlatformSettingDto>> GetSettingsAsync(CancellationToken ct = default) =>
|
||||||
|
Task.FromResult<IReadOnlyList<PlatformSettingDto>>([]);
|
||||||
|
|
||||||
|
public Task<string?> GetSettingAsync(string key, CancellationToken ct = default) =>
|
||||||
|
Task.FromResult<string?>(null);
|
||||||
|
|
||||||
|
public Task<IReadOnlyList<PlatformFeatureDto>> GetFeaturesAsync(CancellationToken ct = default) =>
|
||||||
|
Task.FromResult<IReadOnlyList<PlatformFeatureDto>>([]);
|
||||||
|
|
||||||
|
public Task<IReadOnlyDictionary<string, bool>> GetEffectiveFeaturesForCafeAsync(
|
||||||
|
string cafeId, PlanTier planTier, CancellationToken ct = default) =>
|
||||||
|
Task.FromResult<IReadOnlyDictionary<string, bool>>(new Dictionary<string, bool>());
|
||||||
|
|
||||||
|
public Task<bool> IsFeatureEnabledForCafeAsync(
|
||||||
|
string cafeId, PlanTier planTier, string featureKey, CancellationToken ct = default) =>
|
||||||
|
Task.FromResult(true);
|
||||||
|
|
||||||
|
public void InvalidateCache() { }
|
||||||
|
}
|
||||||
@@ -120,7 +120,7 @@ public class QrMenuTests
|
|||||||
var http = new HttpContextAccessor();
|
var http = new HttpContextAccessor();
|
||||||
var media = new NoOpMediaStorageService();
|
var media = new NoOpMediaStorageService();
|
||||||
var reviews = new ReviewService(db, abuse, http, media);
|
var reviews = new ReviewService(db, abuse, http, media);
|
||||||
var publicSvc = new PublicService(db, orders, reviews, kds, branchMenu, identity, abuse, http);
|
var publicSvc = new PublicService(db, orders, reviews, kds, branchMenu, identity, abuse, http, new NoOpPlatformCatalogService());
|
||||||
|
|
||||||
return (db, tables, publicSvc, cafeId, branchId, tableId, itemA, itemB, qrCode);
|
return (db, tables, publicSvc, cafeId, branchId, tableId, itemA, itemB, qrCode);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ export function QrGuestMenu({ code }: QrGuestMenuProps) {
|
|||||||
const [tableOrders, setTableOrders] = useState<GuestOrderRef[]>([]);
|
const [tableOrders, setTableOrders] = useState<GuestOrderRef[]>([]);
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [menuTheme, setMenuTheme] = useState<CafeTheme | null>(null);
|
const [menuTheme, setMenuTheme] = useState<CafeTheme | null>(null);
|
||||||
|
const [showWatermark, setShowWatermark] = useState(false);
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
const [view3dItem, setView3dItem] = useState<QrPublicMenuItem | null>(null);
|
const [view3dItem, setView3dItem] = useState<QrPublicMenuItem | null>(null);
|
||||||
const [security, setSecurity] = useState<PublicSecurityConfig | null>(null);
|
const [security, setSecurity] = useState<PublicSecurityConfig | null>(null);
|
||||||
@@ -111,6 +112,7 @@ export function QrGuestMenu({ code }: QrGuestMenuProps) {
|
|||||||
const cats = menu.categories ?? [];
|
const cats = menu.categories ?? [];
|
||||||
setCategories(cats);
|
setCategories(cats);
|
||||||
setMenuTheme(normalizeCafeTheme(menu.theme ?? undefined));
|
setMenuTheme(normalizeCafeTheme(menu.theme ?? undefined));
|
||||||
|
setShowWatermark(menu.showWatermark ?? false);
|
||||||
setActiveCategory(QR_ALL_CATEGORY_ID);
|
setActiveCategory(QR_ALL_CATEGORY_ID);
|
||||||
if (cats.length === 0) {
|
if (cats.length === 0) {
|
||||||
setError(t("emptyMenu"));
|
setError(t("emptyMenu"));
|
||||||
@@ -565,6 +567,16 @@ export function QrGuestMenu({ code }: QrGuestMenuProps) {
|
|||||||
view3d: t("view3d"),
|
view3d: t("view3d"),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{showWatermark ? (
|
||||||
|
<a
|
||||||
|
href="https://meezi.ir"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center justify-center gap-1 py-5 text-xs qr-muted opacity-70"
|
||||||
|
>
|
||||||
|
ساختهشده با <span className="font-bold">میزی</span>
|
||||||
|
</a>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
{totalItems > 0 ? (
|
{totalItems > 0 ? (
|
||||||
<div className="pointer-events-none fixed inset-x-0 bottom-[3.25rem] z-40 mx-auto max-w-md px-3 pb-1">
|
<div className="pointer-events-none fixed inset-x-0 bottom-[3.25rem] z-40 mx-auto max-w-md px-3 pb-1">
|
||||||
|
|||||||
@@ -53,6 +53,8 @@ export type QrPublicMenu = {
|
|||||||
slug: string;
|
slug: string;
|
||||||
theme?: CafeTheme | null;
|
theme?: CafeTheme | null;
|
||||||
categories: QrPublicMenuCategory[];
|
categories: QrPublicMenuCategory[];
|
||||||
|
/** Free plan shows the Meezi watermark under the menu; paid plans hide it. */
|
||||||
|
showWatermark?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type QrCartLine = {
|
export type QrCartLine = {
|
||||||
|
|||||||
Reference in New Issue
Block a user