# Meezi — QR Guest Menu + Branch Visual Identity + Tax Inheritance > Copy-paste into Cursor. Based on the bug at /q/demo_table_01 and the branching decisions. > Three self-contained PRs. Start with PR-1 (the broken QR flow). --- ## What's broken and what's new | Area | Status | Work | |------|--------|------| | `/q/[code]` → shows menu | ❌ Broken | Full fix in PR-1 | | Guest can browse menu by category | ❌ Missing | PR-1 | | Guest can place order from QR | ❌ Missing | PR-1 | | Order appears in dashboard panel | ❌ Missing | PR-1 | | Branch has own menu (from parent catalog) | ⚠️ Partial | PR-1 uses BranchMenuItemOverride | | Parent defines child branch tax rates | ❌ Missing | PR-2 | | Parent defines visual identity (colors, icon) | ❌ Missing | PR-3 | --- ## PR-1 — Fix QR Guest Menu: Full Flow ### The flow (end to end) ``` Guest scans QR on table ↓ /q/demo_table_01 (Next.js public page) ↓ GET /api/q/demo_table_01 → resolves table → branch → cafe → returns: { cafeId, branchId, tableId, tableName, branchName, cafeName } ↓ Guest sees: branch name, menu categories, items with prices ↓ Guest picks items → taps "سفارش" (Order) ↓ POST /api/public/{cafeId}/branches/{branchId}/orders Body: { tableId, guestName?, guestPhone?, items: [{menuItemId, qty, note?}] } ↓ Order created → appears in dashboard POS board immediately (SignalR) ↓ Guest sees: "سفارش شما ثبت شد ✓" confirmation screen ``` --- ### STEP 1 — Fix the public QR resolver endpoint **File: `src/Meezi.API/Controllers/QrController.cs`** The current endpoint likely returns minimal data. Expand it: ```csharp [AllowAnonymous] [HttpGet("/api/q/{qrCode}")] public async Task ResolveQr(string qrCode, CancellationToken ct) { // Find table by QrCode field var table = await _db.Tables .Include(t => t.Branch) .ThenInclude(b => b.Cafe) .Include(t => t.Branch) .ThenInclude(b => b.BranchSettings) .FirstOrDefaultAsync(t => t.QrCode == qrCode && t.IsActive, ct); if (table is null) return NotFound(ApiError.Create("TABLE_NOT_FOUND")); // Load branch visual identity (colors, icon) — PR-3 adds this var identity = table.Branch.VisualIdentity; // may be null until PR-3 return Ok(new QrResolveDto { TableId = table.Id, TableName = table.Name, BranchId = table.BranchId, BranchName = table.Branch.Name, CafeId = table.Branch.CafeId, CafeName = table.Branch.Cafe.Name, // Visual identity — fallback to cafe defaults if branch hasn't set own PrimaryColor = identity?.PrimaryColor ?? "#C47B2B", // warm coffee default LogoUrl = identity?.LogoUrl ?? table.Branch.Cafe.LogoUrl, WelcomeText = table.Branch.BranchSettings?.WelcomeText ?? table.Branch.Cafe.WelcomeText ?? "خوش آمدید", }); } ``` --- ### STEP 2 — Public branch menu endpoint **File: `src/Meezi.API/Controllers/PublicController.cs`** Add (no auth — `[AllowAnonymous]` already on controller): ```csharp /// Returns the effective menu for a branch, visible to guests. [HttpGet("/api/public/{cafeId}/branches/{branchId}/menu")] public async Task GetBranchMenu( Guid cafeId, Guid branchId, CancellationToken ct) { // Validate branch belongs to cafe var branch = await _db.Branches .FirstOrDefaultAsync(b => b.Id == branchId && b.CafeId == cafeId && b.IsActive, ct); if (branch is null) return NotFound(ApiError.Create("BRANCH_NOT_FOUND")); // Load all active menu items for this cafe var items = await _db.MenuItems .Include(m => m.Category) .Where(m => m.CafeId == cafeId && m.IsActive) .ToListAsync(ct); // Load branch overrides var overrides = await _db.BranchMenuItemOverrides .Where(o => o.BranchId == branchId && o.CafeId == cafeId) .ToDictionaryAsync(o => o.MenuItemId, ct); // Resolve effective menu — exclude unavailable items var resolved = items .Where(item => !overrides.TryGetValue(item.Id, out var ov) || ov.IsAvailable) .Select(item => { overrides.TryGetValue(item.Id, out var ov); return new PublicMenuItemDto { Id = item.Id, CategoryId = item.CategoryId, CategoryName = item.Category.Name, CategorySort = item.Category.SortOrder, Name = item.Name, Description = item.Description, ImageUrl = item.ImageUrl, EffectivePrice = ov?.PriceOverride ?? item.BasePrice, Tags = item.Tags, IsAvailable = true, SortOrder = ov?.SortOrderOverride ?? item.SortOrder, }; }) .OrderBy(i => i.CategorySort) .ThenBy(i => i.SortOrder) .GroupBy(i => new { i.CategoryId, i.CategoryName, i.CategorySort }) .Select(g => new PublicMenuCategoryDto { Id = g.Key.CategoryId, Name = g.Key.CategoryName, Sort = g.Key.CategorySort, Items = g.ToList(), }) .ToList(); return Ok(ApiResponse>.Ok(resolved)); } ``` --- ### STEP 3 — Public order placement endpoint ```csharp /// Guest places an order from QR menu. [HttpPost("/api/public/{cafeId}/branches/{branchId}/orders")] public async Task PlaceGuestOrder( Guid cafeId, Guid branchId, [FromBody] PlaceGuestOrderRequest request, CancellationToken ct) { // Validate table belongs to this branch var table = await _db.Tables .FirstOrDefaultAsync(t => t.Id == request.TableId && t.BranchId == branchId && t.CafeId == cafeId && t.IsActive, ct); if (table is null) return BadRequest(ApiError.Create("TABLE_NOT_FOUND")); if (table.IsCleaning) return BadRequest(ApiError.Create("TABLE_CLEANING")); // Validate items exist and are available at this branch var itemIds = request.Items.Select(i => i.MenuItemId).ToList(); var menuItems = await _db.MenuItems .Where(m => itemIds.Contains(m.Id) && m.CafeId == cafeId && m.IsActive) .ToListAsync(ct); if (menuItems.Count != itemIds.Distinct().Count()) return BadRequest(ApiError.Create("INVALID_MENU_ITEMS")); // Load price overrides var overrides = await _db.BranchMenuItemOverrides .Where(o => o.BranchId == branchId && itemIds.Contains(o.MenuItemId)) .ToDictionaryAsync(o => o.MenuItemId, ct); // Find or create open order for this table (merge logic — same as POS) var existingOrder = await _db.Orders .Include(o => o.Items) .FirstOrDefaultAsync(o => o.TableId == table.Id && o.CafeId == cafeId && o.Status == OrderStatus.Open, ct); var order = existingOrder ?? new Order { Id = Guid.NewGuid(), CafeId = cafeId, BranchId = branchId, TableId = table.Id, Status = OrderStatus.Open, Source = OrderSource.GuestQr, // new enum value GuestName = request.GuestName, GuestPhone = request.GuestPhone, CreatedAt = DateTime.UtcNow, }; // Append items foreach (var line in request.Items) { var menuItem = menuItems.First(m => m.Id == line.MenuItemId); overrides.TryGetValue(line.MenuItemId, out var ov); var price = ov?.PriceOverride ?? menuItem.BasePrice; order.Items.Add(new OrderItem { Id = Guid.NewGuid(), MenuItemId = line.MenuItemId, ProductName = menuItem.Name, Quantity = line.Quantity, UnitPrice = price, Notes = line.Notes, Source = ItemSource.GuestQr, }); } order.TotalAmount = order.Items .Where(i => !i.IsVoided) .Sum(i => i.UnitPrice * i.Quantity); if (existingOrder is null) _db.Orders.Add(order); await _db.SaveChangesAsync(ct); // Notify dashboard via SignalR await _boardNotifier.OrderUpdatedAsync(cafeId, order.Id, order.TableId); return Ok(ApiResponse.Ok(new GuestOrderConfirmDto { OrderId = order.Id, OrderNumber = order.OrderNumber, TotalAmount = order.TotalAmount, ItemCount = request.Items.Sum(i => i.Quantity), })); } ``` **DTOs to add:** ```csharp // PlaceGuestOrderRequest.cs public record PlaceGuestOrderRequest( Guid TableId, string? GuestName, string? GuestPhone, List Items ); public record GuestOrderLine(Guid MenuItemId, int Quantity, string? Notes); // GuestOrderConfirmDto.cs public record GuestOrderConfirmDto( Guid OrderId, int OrderNumber, decimal TotalAmount, int ItemCount ); // Add to Order entity: public OrderSource Source { get; set; } = OrderSource.Pos; // OrderSource enum: public enum OrderSource { Pos, GuestQr, Kiosk, SnappFood } // Add to OrderItem: public ItemSource Source { get; set; } = ItemSource.Pos; public enum ItemSource { Pos, GuestQr } ``` --- ### STEP 4 — EF migration ```bash dotnet ef migrations add GuestQrOrderSource \ --project src/Meezi.Infrastructure \ --startup-project src/Meezi.API \ --output-dir Data/Migrations ``` --- ### STEP 5 — Fix `web/dashboard/src/app/q/[code]/page.tsx` Full rewrite of the guest-facing QR page. This is a public page — no auth, no dashboard layout. ```tsx "use client"; import { useEffect, useState } from "react"; import { useParams } from "next/navigation"; type BranchInfo = { tableId: string; tableName: string; branchId: string; branchName: string; cafeId: string; cafeName: string; primaryColor: string; logoUrl?: string; welcomeText: string; }; type MenuItem = { id: string; name: string; description?: string; imageUrl?: string; effectivePrice: number; tags?: string[]; }; type Category = { id: string; name: string; items: MenuItem[] }; type CartItem = { item: MenuItem; qty: number; note?: string }; type Screen = "loading" | "error" | "menu" | "cart" | "confirm" | "success"; export default function QrMenuPage() { const { code } = useParams<{ code: string }>(); const [screen, setScreen] = useState("loading"); const [error, setError] = useState(""); const [branch, setBranch] = useState(null); const [categories, setCategories] = useState([]); const [activeCategory, setActiveCategory] = useState(""); const [cart, setCart] = useState([]); const [guestName, setGuestName] = useState(""); const [guestPhone, setGuestPhone] = useState(""); const [orderId, setOrderId] = useState(""); const [orderNumber, setOrderNumber] = useState(0); const apiBase = process.env.NEXT_PUBLIC_API_URL ?? ""; // Step 1: resolve QR code → branch info useEffect(() => { fetch(`${apiBase}/api/q/${code}`) .then(r => r.ok ? r.json() : Promise.reject(r.status)) .then(data => { setBranch(data); // Step 2: load menu return fetch( `${apiBase}/api/public/${data.cafeId}/branches/${data.branchId}/menu` ); }) .then(r => r.ok ? r.json() : Promise.reject(r.status)) .then(res => { setCategories(res.data ?? []); setActiveCategory(res.data?.[0]?.id ?? ""); setScreen("menu"); }) .catch(() => { setError("میز یافت نشد یا منو در دسترس نیست"); setScreen("error"); }); }, [code]); const totalItems = cart.reduce((s, c) => s + c.qty, 0); const totalPrice = cart.reduce((s, c) => s + c.item.effectivePrice * c.qty, 0); function addToCart(item: MenuItem) { setCart(prev => { const idx = prev.findIndex(c => c.item.id === item.id); if (idx >= 0) { const next = [...prev]; next[idx] = { ...next[idx], qty: next[idx].qty + 1 }; return next; } return [...prev, { item, qty: 1 }]; }); } function removeFromCart(itemId: string) { setCart(prev => { const idx = prev.findIndex(c => c.item.id === itemId); if (idx < 0) return prev; const next = [...prev]; if (next[idx].qty > 1) { next[idx] = { ...next[idx], qty: next[idx].qty - 1 }; } else { next.splice(idx, 1); } return next; }); } async function submitOrder() { if (!branch || cart.length === 0) return; setScreen("loading"); try { const res = await fetch( `${apiBase}/api/public/${branch.cafeId}/branches/${branch.branchId}/orders`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ tableId: branch.tableId, guestName: guestName || null, guestPhone: guestPhone || null, items: cart.map(c => ({ menuItemId: c.item.id, quantity: c.qty, notes: c.note ?? null, })), }), } ); if (!res.ok) throw new Error(); const data = await res.json(); setOrderId(data.data.orderId); setOrderNumber(data.data.orderNumber); setScreen("success"); } catch { setError("خطا در ثبت سفارش. دوباره امتحان کنید"); setScreen("cart"); } } const primary = branch?.primaryColor ?? "#C47B2B"; // ── Screens ────────────────────────────────────────────────────────────── if (screen === "loading") return (

در حال بارگذاری...

); if (screen === "error") return (

😕

{error}

لطفاً دوباره کد QR را اسکن کنید

); if (screen === "success") return (

سفارش ثبت شد!

شماره سفارش: #{orderNumber}

کارکنان به زودی سفارش شما را آماده می‌کنند

); if (screen === "cart") return (
{/* Header */}

سبد خرید

{/* Cart items */} {cart.map(c => (

{c.item.name}

{c.item.effectivePrice.toLocaleString("fa-IR")} تومان

{c.qty}
))} {/* Guest info */}
setGuestName(e.target.value)} placeholder="نام شما (اختیاری)" style={{ width: "100%", padding: "10px 14px", borderRadius: 10, border: "1px solid #ddd", fontSize: 15, marginBottom: 10, boxSizing: "border-box", textAlign: "right" }} /> setGuestPhone(e.target.value)} placeholder="شماره موبایل (اختیاری)" inputMode="tel" style={{ width: "100%", padding: "10px 14px", borderRadius: 10, border: "1px solid #ddd", fontSize: 15, boxSizing: "border-box", textAlign: "right" }} />
{/* Total + submit */}
جمع کل: {totalPrice.toLocaleString("fa-IR")} تومان
); // screen === "menu" return (
{/* Branch header */}
{branch?.logoUrl && ( {branch.cafeName} )}

{branch?.cafeName}

{branch?.branchName}

{branch?.welcomeText} — میز {branch?.tableName}

{/* Category tabs */}
{categories.map(cat => ( ))}
{/* Menu items */}
{categories .filter(cat => cat.id === activeCategory) .flatMap(cat => cat.items) .map(item => { const inCart = cart.find(c => c.item.id === item.id); return (
{item.imageUrl && ( {item.name} )}

{item.name}

{item.description && (

{item.description}

)}
{item.effectivePrice.toLocaleString("fa-IR")} تومان {inCart ? (
{inCart.qty}
) : ( )}
); })}
{/* Floating cart bar */} {totalItems > 0 && (
)}
); } ``` --- ### STEP 6 — Dashboard: show guest QR orders in POS board In `pos-table-board.tsx` — orders with `Source == GuestQr` should show a badge: ```tsx {order.source === "GuestQr" && ( QR سفارش مهمان )} ``` In `OrderDto`, add: `public string Source { get; set; }` (mapped from `OrderSource` enum). --- ### STEP 7 — Tests Add to `tests/Meezi.API.Tests/QrMenuTests.cs`: ``` ✓ ResolveQr_ValidCode_ReturnsBranchInfo ✓ ResolveQr_InvalidCode_ReturnsNotFound ✓ GetBranchMenu_ExcludesUnavailableItems ✓ GetBranchMenu_AppliesBranchPriceOverride ✓ PlaceGuestOrder_ValidItems_CreatesOrder ✓ PlaceGuestOrder_MergesWithExistingOpenOrder ✓ PlaceGuestOrder_CleaningTable_ReturnsTableCleaning ✓ PlaceGuestOrder_InvalidMenuItemForBranch_ReturnsInvalidMenuItems ``` --- ## PR-2 — Parent Branch Tax Inheritance ### The rule (finalized) ``` Cafe (owner) defines: - DefaultTaxRate → applies to ALL branches unless overridden - AllowBranchTaxOverride (bool) — if false, branches CANNOT change their own tax Branch can ONLY change tax if parent AllowBranchTaxOverride == true Otherwise: branch tax is ALWAYS the cafe's DefaultTaxRate, read-only ``` --- ### STEP 1 — Add to CafeSettings ```csharp // src/Meezi.Core/Entities/CafeSettings.cs — add: public decimal DefaultTaxRate { get; set; } = 0m; public bool AllowBranchTaxOverride { get; set; } = false; ``` --- ### STEP 2 — Update EffectiveSettingsService ```csharp public async Task GetEffectiveSettingsAsync( Guid cafeId, Guid branchId, CancellationToken ct) { var cafeSettings = await _db.CafeSettings .FirstOrDefaultAsync(s => s.CafeId == cafeId, ct); var branchSettings = await _db.BranchSettings .FirstOrDefaultAsync(s => s.BranchId == branchId, ct); // Tax: only use branch override if parent explicitly allows it var allowTaxOverride = cafeSettings?.AllowBranchTaxOverride ?? false; var effectiveTaxRate = (allowTaxOverride && branchSettings?.TaxRate != null) ? branchSettings.TaxRate.Value : (cafeSettings?.DefaultTaxRate ?? 0m); return new BranchEffectiveSettingsDto { TaxRate = effectiveTaxRate, TaxRateIsOverridden = allowTaxOverride && branchSettings?.TaxRate != null, TaxRateLocked = !allowTaxOverride, // UI shows lock icon if true // ... other settings }; } ``` --- ### STEP 3 — Guard in BranchSettings PATCH endpoint ```csharp // In BranchSettingsController.PatchAsync — before saving tax rate: if (request.TaxRate.HasValue) { var cafeSettings = await _db.CafeSettings .FirstOrDefaultAsync(s => s.CafeId == _tenant.CafeId, ct); if (cafeSettings?.AllowBranchTaxOverride != true) return BadRequest(ApiResponse.Fail("TAX_OVERRIDE_NOT_ALLOWED", "تغییر نرخ مالیات شعبه توسط مالک غیرفعال شده است")); } ``` --- ### STEP 4 — Dashboard: Tax settings UI In cafe-level settings (owner only): ``` مالیات پیش‌فرض کل کافه: [ 9 ] % ☐ اجازه تغییر نرخ مالیات به مدیران شعبه ``` In branch settings (manager view): ``` نرخ مالیات: 9% 🔒 (تعریف شده توسط مالک) ``` OR if `AllowBranchTaxOverride == true`: ``` نرخ مالیات: [ 12 ] % ✏️ (قابل تغییر) ``` --- ### Migration ```bash dotnet ef migrations add TaxInheritanceControl \ --project src/Meezi.Infrastructure \ --startup-project src/Meezi.API ``` --- ## PR-3 — Visual Identity Per Branch ### What the parent defines for children ``` Cafe (parent) defines the BRAND: - CafeName, LogoUrl - PrimaryColor (hex) ← all branches inherit this - SecondaryColor (hex) - FontFamily (string) ← optional: "Vazir", "IRANSans", "default" - FaviconUrl Branch CAN override for its location: - BranchLogoUrl ← e.g. branch-specific photo - AccentColor ← slight variation - WelcomeText ← "خوش آمدید به شعبه نیاوران" - WifiPassword ← shown on QR menu and receipts Branch CANNOT override: - CafeName (always the brand name) - PrimaryColor (brand consistency) - FontFamily (brand consistency) ``` --- ### STEP 1 — CafeIdentity entity (new) ```csharp // src/Meezi.Core/Entities/CafeIdentity.cs public class CafeIdentity { public Guid Id { get; set; } public Guid CafeId { get; set; } public string PrimaryColor { get; set; } = "#C47B2B"; // warm coffee default public string SecondaryColor { get; set; } = "#F5F0E8"; // light cream public string? FontFamily { get; set; } public string? LogoUrl { get; set; } public string? FaviconUrl { get; set; } public string? IconName { get; set; } // Tabler icon name e.g. "coffee" public DateTime UpdatedAt { get; set; } } ``` --- ### STEP 2 — BranchIdentity entity (new) ```csharp // src/Meezi.Core/Entities/BranchIdentity.cs public class BranchIdentity { public Guid Id { get; set; } public Guid BranchId { get; set; } public Guid CafeId { get; set; } // Branch-level overrides (nulls = use CafeIdentity value) public string? LogoUrl { get; set; } // branch-specific photo public string? AccentColor { get; set; } // slight color variation public string? WelcomeText { get; set; } // greeting on QR menu public string? WifiPassword { get; set; } public string? Address { get; set; } // shown on QR menu footer } ``` --- ### STEP 3 — Resolve effective identity ```csharp // In EffectiveSettingsService or new IdentityService: public async Task GetEffectiveIdentityAsync( Guid cafeId, Guid branchId, CancellationToken ct) { var cafe = await _db.CafeIdentities .FirstOrDefaultAsync(i => i.CafeId == cafeId, ct); var branch = await _db.BranchIdentities .FirstOrDefaultAsync(i => i.BranchId == branchId, ct); return new BranchEffectiveIdentityDto { PrimaryColor = cafe?.PrimaryColor ?? "#C47B2B", // NEVER branch override SecondaryColor = cafe?.SecondaryColor ?? "#F5F0E8", FontFamily = cafe?.FontFamily, LogoUrl = branch?.LogoUrl ?? cafe?.LogoUrl, // branch photo first IconName = cafe?.IconName ?? "coffee", WelcomeText = branch?.WelcomeText ?? "خوش آمدید", WifiPassword = branch?.WifiPassword, Address = branch?.Address, }; } ``` --- ### STEP 4 — Endpoints ``` // Cafe identity (owner only): GET /api/cafes/{cafeId}/identity PUT /api/cafes/{cafeId}/identity Body: { primaryColor, secondaryColor, fontFamily, iconName, logoUrl, faviconUrl } // Branch identity (owner or branch manager): GET /api/cafes/{cafeId}/branches/{branchId}/identity PUT /api/cafes/{cafeId}/branches/{branchId}/identity Body: { logoUrl, welcomeText, wifiPassword, address } // Note: primaryColor NOT in body — branch cannot change it // Public (used by QR menu): GET /api/public/{cafeId}/branches/{branchId}/identity → Returns BranchEffectiveIdentityDto ``` --- ### STEP 5 — Dashboard: Visual Identity Settings Page **File: `web/dashboard/src/components/settings/identity-settings.tsx` (new)** Owner view (cafe-level): ``` Brand Color: [■ #C47B2B] color picker Secondary Color: [■ #F5F0E8] Logo: [upload] Icon: icon picker (grid of Tabler icon names: coffee, cup, utensils, store, ...) Font: dropdown [پیش‌فرض | Vazir | IRANSans] Preview: shows QR menu header with current settings ``` Branch manager view: ``` رنگ برند: ■ #C47B2B 🔒 (تعریف شده توسط مالک — قابل تغییر نیست) لوگو شعبه: [upload] ← branch-specific photo متن خوش‌آمدگویی: [___________] رمز WiFi: [___________] آدرس: [___________] ``` --- ### STEP 6 — Apply identity to QR menu Update the QR resolver endpoint (STEP 1 of PR-1) to also call `GetEffectiveIdentityAsync` and include the color/logo in `QrResolveDto`. The guest menu page already uses `branch.primaryColor` for all buttons and accents — it will automatically reflect the brand color. --- ### Migration ```bash dotnet ef migrations add BranchVisualIdentity \ --project src/Meezi.Infrastructure \ --startup-project src/Meezi.API ``` --- ## Execution Order ``` PR-1 feature/qr-guest-menu ← fix the broken flow FIRST (2 day) PR-2 feature/tax-inheritance ← simple, 4 hours PR-3 feature/visual-identity ← after PR-1 (QR menu uses the colors), 1 day ``` ## New Error Codes | Code | Meaning | |------|---------| | `INVALID_MENU_ITEMS` | Guest submitted item not in branch menu | | `TAX_OVERRIDE_NOT_ALLOWED` | Branch tried to set tax but parent locked it | | `BRANCH_IDENTITY_NOT_FOUND` | Identity record missing (auto-create on first save) | ## New i18n Strings ```json // fa.json additions: "qrMenu": { "welcome": "خوش آمدید", "tableLabel": "میز", "addToCart": "افزودن", "viewCart": "مشاهده سبد خرید", "placeOrder": "ثبت سفارش", "orderPlaced": "سفارش ثبت شد!", "orderNumber": "شماره سفارش", "guestName": "نام شما (اختیاری)", "guestPhone": "شماره موبایل (اختیاری)", "addMoreItems": "افزودن آیتم دیگر", "tableNotFound": "میز یافت نشد", "loadError": "خطا در بارگذاری", "orderError": "خطا در ثبت سفارش. دوباره امتحان کنید", "subtotal": "جمع کل", "guestQrBadge": "سفارش QR مهمان" }, "identity": { "brandColor": "رنگ برند", "secondaryColor": "رنگ ثانویه", "logo": "لوگو", "icon": "آیکون", "font": "فونت", "branchLogo": "لوگو شعبه", "welcomeText": "متن خوش‌آمدگویی", "wifiPassword": "رمز WiFi", "lockedByOwner": "تعریف شده توسط مالک — قابل تغییر نیست", "preview": "پیش‌نمایش" }, "tax": { "defaultTaxRate": "نرخ مالیات پیش‌فرض", "allowBranchOverride": "اجازه تغییر نرخ مالیات به مدیران شعبه", "lockedByOwner": "نرخ مالیات توسط مالک قفل شده است", "overrideNotAllowed": "تغییر نرخ مالیات شعبه توسط مالک غیرفعال شده است" } ``` --- *Start with PR-1 — the QR menu fix. The other two are independent and can run in parallel after PR-1 merges.*