diff --git a/data/cafe-theme-presets.json b/data/cafe-theme-presets.json new file mode 100644 index 0000000..ec5b51f --- /dev/null +++ b/data/cafe-theme-presets.json @@ -0,0 +1,14 @@ +{ + "version": 1, + "palettes": [ + "meezi-green", "ocean-blue", "royal-purple", "sunset-orange", "rose-blush", + "charcoal-gold", "espresso", "forest", "midnight", "coral", "gold-luxury", + "mint-fresh", "wine-bar", "slate-modern", "cherry", "teal-wave", "sand-cafe" + ], + "panelStyles": ["flat", "modern", "glass", "minimal", "bold", "soft", "elevated", "outline"], + "menuStyles": ["cards", "compact", "grid", "list", "magazine", "classic"], + "menuTextures": ["none", "paper", "linen", "dots", "grid", "marble", "wood", "warm"], + "densities": ["compact", "comfortable", "spacious"], + "radius": ["none", "sm", "md", "lg", "full"], + "customColorKeys": ["primary", "secondary", "accent", "background", "surface", "text", "textMuted", "destructive", "success"] +} diff --git a/data/category-icon-presets.json b/data/category-icon-presets.json new file mode 100644 index 0000000..9b46e68 --- /dev/null +++ b/data/category-icon-presets.json @@ -0,0 +1,46 @@ +{ + "version": 2, + "styles": ["flat", "modern", "real", "minimal", "outline", "soft", "bold", "gradient", "pastel", "duotone"], + "presets": [ + { "id": "drinks-hot", "kind": "drink" }, + { "id": "drinks-cold", "kind": "drink" }, + { "id": "drinks-tea", "kind": "drink" }, + { "id": "drinks-juice", "kind": "drink" }, + { "id": "drinks-milkshake", "kind": "drink" }, + { "id": "drinks-alcohol", "kind": "drink" }, + { "id": "drinks-beer", "kind": "drink" }, + { "id": "breakfast", "kind": "food" }, + { "id": "food-mains", "kind": "food" }, + { "id": "food-fastfood", "kind": "food" }, + { "id": "food-rice", "kind": "food" }, + { "id": "pasta-pizza", "kind": "food" }, + { "id": "dessert", "kind": "food" }, + { "id": "ice-cream", "kind": "food" }, + { "id": "bakery", "kind": "food" }, + { "id": "salad", "kind": "food" }, + { "id": "grill", "kind": "food" }, + { "id": "seafood", "kind": "food" }, + { "id": "snacks", "kind": "food" }, + { "id": "snacks-sweet", "kind": "food" }, + { "id": "appetizers", "kind": "food" }, + { "id": "vegan", "kind": "food" }, + { "id": "fruits", "kind": "food" }, + { "id": "specials", "kind": "food" }, + { "id": "chef-special", "kind": "food" }, + { "id": "generic", "kind": "food" } + ], + "emojiGroups": [ + "hotDrinks", + "coldDrinks", + "breakfast", + "mains", + "pastaPizza", + "desserts", + "salads", + "seafoodGrill", + "snacks", + "vegan", + "specials", + "general" + ] +} diff --git a/data/demo-credentials.json b/data/demo-credentials.json new file mode 100644 index 0000000..c227ba8 --- /dev/null +++ b/data/demo-credentials.json @@ -0,0 +1,34 @@ +{ + "version": 1, + "environment": "development", + "cafe": { + "id": "cafe_demo_001", + "slug": "demo-cafe", + "name": "کافه دمو" + }, + "branch": { + "id": "branch_demo_main", + "name": "شعبه اصلی" + }, + "auth": { + "note": "در محیط Development با OTP توسعه وارد شوید (کد در لاگ API).", + "otpDevBypass": true + }, + "systemAdmin": { + "phone": "09120000001", + "note": "ورود مدیر سامانه: /fa/admin/login — OTP در لاگ API" + }, + "employees": [ + { "id": "emp_demo_owner", "name": "مدیر دمو", "phone": "09121234567", "role": "Owner" }, + { "id": "emp_demo_manager", "name": "مدیر شعبه", "phone": "09121111111", "role": "Manager" }, + { "id": "emp_demo_cashier", "name": "صندوقدار", "phone": "09122222222", "role": "Cashier" }, + { "id": "emp_demo_waiter", "name": "گارسون", "phone": "09123333333", "role": "Waiter" }, + { "id": "emp_demo_waiter2", "name": "گارسون ۲", "phone": "09124444444", "role": "Waiter" }, + { "id": "emp_demo_chef", "name": "آشپز", "phone": "09125555555", "role": "Chef" }, + { "id": "emp_demo_delivery", "name": "پیک", "phone": "09126666666", "role": "Delivery" } + ], + "publicQr": { + "tableQr": "demo_table_01", + "discoverSlug": "demo-cafe" + } +} diff --git a/data/demo-menu-food101.json b/data/demo-menu-food101.json new file mode 100644 index 0000000..3afb954 --- /dev/null +++ b/data/demo-menu-food101.json @@ -0,0 +1,644 @@ +{ + "version": 1, + "cafeId": "cafe_demo_001", + "categories": [ + { + "Id": "cat_demo_drinks", + "Name": "\u0646\u0648\u0634\u06CC\u062F\u0646\u06CC \u06AF\u0631\u0645", + "NameEn": "Hot drinks", + "NameAr": "\u0645\u0634\u0631\u0648\u0628\u0627\u062A \u0633\u0627\u062E\u0646\u0629", + "SortOrder": 1, + "Icon": null, + "IconPresetId": "drinks-hot", + "IconStyle": "flat" + }, + { + "Id": "cat_demo_cold", + "Name": "\u0646\u0648\u0634\u06CC\u062F\u0646\u06CC \u0633\u0631\u062F", + "NameEn": "Cold drinks", + "NameAr": "\u0645\u0634\u0631\u0648\u0628\u0627\u062A \u0628\u0627\u0631\u062F\u0629", + "SortOrder": 2, + "Icon": null, + "IconPresetId": "drinks-cold", + "IconStyle": "flat" + }, + { + "Id": "cat_demo_breakfast", + "Name": "\u0635\u0628\u062D\u0627\u0646\u0647", + "NameEn": "Breakfast", + "NameAr": "\u0641\u0637\u0648\u0631", + "SortOrder": 3, + "Icon": null, + "IconPresetId": "breakfast", + "IconStyle": "flat" + }, + { + "Id": "cat_demo_food", + "Name": "\u063A\u0630\u0627 \u0648 \u067E\u06CC\u0634\u200C\u063A\u0630\u0627", + "NameEn": "Food \u0026 snacks", + "NameAr": "\u0637\u0639\u0627\u0645", + "SortOrder": 4, + "Icon": null, + "IconPresetId": "food-mains", + "IconStyle": "flat" + }, + { + "Id": "cat_demo_pasta", + "Name": "\u067E\u0627\u0633\u062A\u0627 \u0648 \u067E\u06CC\u062A\u0632\u0627", + "NameEn": "Pasta \u0026 pizza", + "NameAr": "\u0645\u0639\u0643\u0631\u0648\u0646\u0629 \u0648\u0628\u064A\u062A\u0632\u0627", + "SortOrder": 5, + "Icon": null, + "IconPresetId": "pasta-pizza", + "IconStyle": "flat" + }, + { + "Id": "cat_demo_dessert", + "Name": "\u062F\u0633\u0631", + "NameEn": "Desserts", + "NameAr": "\u062D\u0644\u0648\u064A\u0627\u062A", + "SortOrder": 6, + "Icon": null, + "IconPresetId": "dessert", + "IconStyle": "flat" + } + ], + "items": [ + { + "Id": "item_demo_espresso", + "CategoryId": "cat_demo_drinks", + "Name": "\u0627\u0633\u067E\u0631\u0633\u0648", + "NameEn": "Espresso", + "NameAr": "\u0625\u0633\u0628\u0631\u064A\u0633\u0648", + "Description": "\u062F\u0648\u0628\u0644 \u06CC\u0627 \u0633\u06CC\u0646\u06AF\u0644", + "priceToman": 65000, + "DiscountPercent": 0, + "Food101Class": "espresso", + "imageUrl": "https://images.unsplash.com/photo-1509042239860-f550ce710b93?w=600\u0026auto=format\u0026fit=crop" + }, + { + "Id": "item_demo_americano", + "CategoryId": "cat_demo_drinks", + "Name": "\u0622\u0645\u0631\u06CC\u06A9\u0627\u0646\u0648", + "NameEn": "Americano", + "NameAr": "\u0623\u0645\u0631\u064A\u0643\u0627\u0646\u0648", + "Description": null, + "priceToman": 75000, + "DiscountPercent": 0, + "Food101Class": "cappuccino", + "imageUrl": "https://images.unsplash.com/photo-1572442388796-11668a67e3d0?w=600" + }, + { + "Id": "item_demo_latte", + "CategoryId": "cat_demo_drinks", + "Name": "\u0644\u0627\u062A\u0647", + "NameEn": "Latte", + "NameAr": "\u0644\u0627\u062A\u064A\u0647", + "Description": "\u0634\u06CC\u0631 \u0628\u062E\u0627\u0631 \u06AF\u0631\u0641\u062A\u0647", + "priceToman": 120000, + "DiscountPercent": 0, + "Food101Class": "latte", + "imageUrl": "https://images.unsplash.com/photo-1461023058943-07fcbe16d735?w=600" + }, + { + "Id": "item_demo_cappuccino", + "CategoryId": "cat_demo_drinks", + "Name": "\u06A9\u0627\u067E\u0648\u0686\u06CC\u0646\u0648", + "NameEn": "Cappuccino", + "NameAr": "\u0643\u0627\u0628\u062A\u0634\u064A\u0646\u0648", + "Description": null, + "priceToman": 110000, + "DiscountPercent": 10, + "Food101Class": "cappuccino", + "imageUrl": "https://images.unsplash.com/photo-1572442388796-11668a67e3d0?w=600" + }, + { + "Id": "item_demo_mocha", + "CategoryId": "cat_demo_drinks", + "Name": "\u0645\u0648\u06A9\u0627", + "NameEn": "Mocha", + "NameAr": "\u0645\u0648\u0643\u0627", + "Description": "\u0634\u06A9\u0644\u0627\u062A \u0648 \u0642\u0647\u0648\u0647", + "priceToman": 135000, + "DiscountPercent": 0, + "Food101Class": "mocha", + "imageUrl": "https://images.unsplash.com/photo-1577887233537-a81b387b125e?w=600" + }, + { + "Id": "item_demo_tea", + "CategoryId": "cat_demo_drinks", + "Name": "\u0686\u0627\u06CC \u0645\u0627\u0633\u0627\u0644\u0627", + "NameEn": "Masala tea", + "NameAr": "\u0634\u0627\u064A \u0645\u0627\u0633\u0627\u0644\u0627", + "Description": null, + "priceToman": 85000, + "DiscountPercent": 0, + "Food101Class": "miso_soup", + "imageUrl": "https://images.unsplash.com/photo-1556679343-c7306c1976bc?w=600" + }, + { + "Id": "item_demo_iced_latte", + "CategoryId": "cat_demo_cold", + "Name": "\u0622\u06CC\u0633 \u0644\u0627\u062A\u0647", + "NameEn": "Iced latte", + "NameAr": "\u0622\u064A\u0633 \u0644\u0627\u062A\u064A\u0647", + "Description": null, + "priceToman": 130000, + "DiscountPercent": 0, + "Food101Class": "iced_coffee", + "imageUrl": "https://images.unsplash.com/photo-1517487881594-2787aeee8f58?w=600\u0026auto=format\u0026fit=crop" + }, + { + "Id": "item_demo_cold_brew", + "CategoryId": "cat_demo_cold", + "Name": "\u06A9\u0648\u0644\u062F \u0628\u0631\u0648", + "NameEn": "Cold brew", + "NameAr": "\u0643\u0648\u0644\u062F \u0628\u0631\u0648", + "Description": null, + "priceToman": 140000, + "DiscountPercent": 0, + "Food101Class": "iced_coffee", + "imageUrl": "https://images.unsplash.com/photo-1517487881594-2787aeee8f58?w=600\u0026auto=format\u0026fit=crop" + }, + { + "Id": "item_demo_lemonade", + "CategoryId": "cat_demo_cold", + "Name": "\u0644\u06CC\u0645\u0648\u0646\u0627\u062F", + "NameEn": "Lemonade", + "NameAr": "\u0644\u064A\u0645\u0648\u0646\u0627\u062F\u0629", + "Description": "\u062A\u0627\u0632\u0647", + "priceToman": 95000, + "DiscountPercent": 0, + "Food101Class": "lemonade", + "imageUrl": "https://images.unsplash.com/photo-1523672990561-64c16245f769?w=600" + }, + { + "Id": "item_demo_smoothie", + "CategoryId": "cat_demo_cold", + "Name": "\u0627\u0633\u0645\u0648\u062A\u06CC \u062A\u0648\u062A", + "NameEn": "Berry smoothie", + "NameAr": "\u0633\u0645\u0648\u0630\u064A", + "Description": null, + "priceToman": 150000, + "DiscountPercent": 15, + "Food101Class": "smoothie", + "imageUrl": "https://images.unsplash.com/photo-1505252585463-0433371f7f6b?w=600" + }, + { + "Id": "item_demo_croissant", + "CategoryId": "cat_demo_breakfast", + "Name": "\u06A9\u0631\u0648\u0633\u0627\u0646", + "NameEn": "Croissant", + "NameAr": "\u0643\u0631\u0648\u0627\u0633\u0627\u0646", + "Description": "\u06A9\u0631\u0647\u200C\u0627\u06CC", + "priceToman": 75000, + "DiscountPercent": 0, + "Food101Class": "croque_madame", + "imageUrl": "https://images.unsplash.com/photo-1555507036342-9231d37c10f3?w=600" + }, + { + "Id": "item_demo_omelette", + "CategoryId": "cat_demo_breakfast", + "Name": "\u0627\u0645\u0644\u062A", + "NameEn": "Omelette", + "NameAr": "\u0623\u0648\u0645\u0644\u064A\u062A", + "Description": "\u0646\u0627\u0646 \u0633\u0646\u06AF\u06A9", + "priceToman": 145000, + "DiscountPercent": 0, + "Food101Class": "omelette", + "imageUrl": "https://images.unsplash.com/photo-1525351484343-752d43d363f1?w=600" + }, + { + "Id": "item_demo_avocado", + "CategoryId": "cat_demo_breakfast", + "Name": "\u062A\u0648\u0633\u062A \u0622\u0648\u0648\u06A9\u0627\u062F\u0648", + "NameEn": "Avocado toast", + "NameAr": "\u062A\u0648\u0633\u062A \u0623\u0641\u0648\u0643\u0627\u062F\u0648", + "Description": null, + "priceToman": 185000, + "DiscountPercent": 0, + "Food101Class": "avocado_toast", + "imageUrl": "https://images.unsplash.com/photo-1541519221064-49632fb3380e?w=600" + }, + { + "Id": "item_demo_pancakes", + "CategoryId": "cat_demo_breakfast", + "Name": "\u067E\u0646\u06A9\u06CC\u06A9", + "NameEn": "Pancakes", + "NameAr": "\u0641\u0637\u0627\u0626\u0631", + "Description": "\u0639\u0633\u0644 \u0648 \u06A9\u0631\u0647", + "priceToman": 165000, + "DiscountPercent": 0, + "Food101Class": "pancakes", + "imageUrl": "https://images.unsplash.com/photo-1567620905732-2d1ec7ab7440?w=600" + }, + { + "Id": "item_demo_waffles", + "CategoryId": "cat_demo_breakfast", + "Name": "\u0648\u0627\u0641\u0644", + "NameEn": "Waffles", + "NameAr": "\u0648\u0627\u0641\u0644", + "Description": null, + "priceToman": 175000, + "DiscountPercent": 0, + "Food101Class": "waffles", + "imageUrl": "https://images.unsplash.com/photo-1567818735240-7acbb4b7e34c?w=600" + }, + { + "Id": "item_demo_french_toast", + "CategoryId": "cat_demo_breakfast", + "Name": "\u0641\u0631\u0646\u0686 \u062A\u0633\u062A", + "NameEn": "French toast", + "NameAr": "\u062A\u0648\u0633\u062A \u0641\u0631\u0646\u0633\u064A", + "Description": null, + "priceToman": 155000, + "DiscountPercent": 0, + "Food101Class": "french_toast", + "imageUrl": "https://images.unsplash.com/photo-1484723091739-30a329e1f0c4?w=600" + }, + { + "Id": "item_demo_eggs_benedict", + "CategoryId": "cat_demo_breakfast", + "Name": "\u0627\u06AF \u0628\u0646\u062F\u06CC\u06A9\u062A", + "NameEn": "Eggs Benedict", + "NameAr": "\u0628\u064A\u0636 \u0628\u0646\u062F\u064A\u0643\u062A", + "Description": null, + "priceToman": 195000, + "DiscountPercent": 0, + "Food101Class": "eggs_benedict", + "imageUrl": "https://images.unsplash.com/photo-1608039819502-3d5a2a2e0b0e?w=600" + }, + { + "Id": "item_demo_sandwich", + "CategoryId": "cat_demo_food", + "Name": "\u0633\u0627\u0646\u062F\u0648\u06CC\u0686 \u0645\u0631\u063A", + "NameEn": "Chicken sandwich", + "NameAr": "\u0633\u0627\u0646\u062F\u0648\u064A\u062A\u0634 \u062F\u062C\u0627\u062C", + "Description": null, + "priceToman": 195000, + "DiscountPercent": 0, + "Food101Class": "club_sandwich", + "imageUrl": "https://images.unsplash.com/photo-1528735602780-2552fd46c7af?w=600" + }, + { + "Id": "item_demo_salad", + "CategoryId": "cat_demo_food", + "Name": "\u0633\u0627\u0644\u0627\u062F \u0633\u0632\u0627\u0631", + "NameEn": "Caesar salad", + "NameAr": "\u0633\u0644\u0637\u0629 \u0633\u064A\u0632\u0631", + "Description": null, + "priceToman": 175000, + "DiscountPercent": 0, + "Food101Class": "caesar_salad", + "imageUrl": "https://images.unsplash.com/photo-1546793665-c74683f339c1?w=600" + }, + { + "Id": "item_demo_greek_salad", + "CategoryId": "cat_demo_food", + "Name": "\u0633\u0627\u0644\u0627\u062F \u06CC\u0648\u0646\u0627\u0646\u06CC", + "NameEn": "Greek salad", + "NameAr": "\u0633\u0644\u0637\u0629 \u064A\u0648\u0646\u0627\u0646\u064A\u0629", + "Description": null, + "priceToman": 168000, + "DiscountPercent": 0, + "Food101Class": "greek_salad", + "imageUrl": "https://images.unsplash.com/photo-1540189549336-e6e99c3679fe?w=600" + }, + { + "Id": "item_demo_burger", + "CategoryId": "cat_demo_food", + "Name": "\u0647\u0645\u0628\u0631\u06AF\u0631", + "NameEn": "Burger", + "NameAr": "\u0628\u0631\u062C\u0631", + "Description": "\u06F1\u06F5\u06F0 \u06AF\u0631\u0645 \u06AF\u0648\u0634\u062A", + "priceToman": 245000, + "DiscountPercent": 0, + "Food101Class": "hamburger", + "imageUrl": "https://images.unsplash.com/photo-1568901346375-23c9450c58cd?w=600" + }, + { + "Id": "item_demo_steak", + "CategoryId": "cat_demo_food", + "Name": "\u0627\u0633\u062A\u06CC\u06A9", + "NameEn": "Steak", + "NameAr": "\u0633\u062A\u064A\u0643", + "Description": "medium", + "priceToman": 385000, + "DiscountPercent": 0, + "Food101Class": "steak", + "imageUrl": "https://images.unsplash.com/photo-1600891963295-d66a269b9202?w=600" + }, + { + "Id": "item_demo_salmon", + "CategoryId": "cat_demo_food", + "Name": "\u0633\u0627\u0644\u0645\u0648\u0646 \u06AF\u0631\u06CC\u0644", + "NameEn": "Grilled salmon", + "NameAr": "\u0633\u0644\u0645\u0648\u0646 \u0645\u0634\u0648\u064A", + "Description": null, + "priceToman": 320000, + "DiscountPercent": 0, + "Food101Class": "grilled_salmon", + "imageUrl": "https://images.unsplash.com/photo-1467003909585-2f8a72700288?w=600" + }, + { + "Id": "item_demo_tacos", + "CategoryId": "cat_demo_food", + "Name": "\u062A\u0627\u06A9\u0648", + "NameEn": "Tacos", + "NameAr": "\u062A\u0627\u0643\u0648", + "Description": "\u0633\u0647 \u0639\u062F\u062F", + "priceToman": 210000, + "DiscountPercent": 0, + "Food101Class": "tacos", + "imageUrl": "https://images.unsplash.com/photo-1565299585323-38174c4aab1e?w=600" + }, + { + "Id": "item_demo_shawarma", + "CategoryId": "cat_demo_food", + "Name": "\u0634\u0627\u0648\u0631\u0645\u0627", + "NameEn": "Shawarma", + "NameAr": "\u0634\u0627\u0648\u0631\u0645\u0627", + "Description": null, + "priceToman": 185000, + "DiscountPercent": 0, + "Food101Class": "shawarma", + "imageUrl": "https://images.unsplash.com/photo-1529006557810-274adbcb39d8?w=600" + }, + { + "Id": "item_demo_falafel", + "CategoryId": "cat_demo_food", + "Name": "\u0641\u0644\u0627\u0641\u0644", + "NameEn": "Falafel", + "NameAr": "\u0641\u0644\u0627\u0641\u0644", + "Description": "\u06F6 \u0639\u062F\u062F", + "priceToman": 125000, + "DiscountPercent": 0, + "Food101Class": "falafel", + "imageUrl": "https://images.unsplash.com/photo-1601050690597-df5748fb5cee?w=600" + }, + { + "Id": "item_demo_hummus", + "CategoryId": "cat_demo_food", + "Name": "\u062D\u0645\u0635", + "NameEn": "Hummus", + "NameAr": "\u062D\u0645\u0635", + "Description": "\u0646\u0627\u0646 \u067E\u06CC\u062A\u0627", + "priceToman": 95000, + "DiscountPercent": 0, + "Food101Class": "hummus", + "imageUrl": "https://images.unsplash.com/photo-1626208082043-e6319abfeec2?w=600" + }, + { + "Id": "item_demo_fries", + "CategoryId": "cat_demo_food", + "Name": "\u0633\u06CC\u0628\u200C\u0632\u0645\u06CC\u0646\u06CC \u0633\u0631\u062E\u200C\u06A9\u0631\u062F\u0647", + "NameEn": "French fries", + "NameAr": "\u0628\u0637\u0627\u0637\u0633 \u0645\u0642\u0644\u064A\u0629", + "Description": null, + "priceToman": 85000, + "DiscountPercent": 0, + "Food101Class": "french_fries", + "imageUrl": "https://images.unsplash.com/photo-1573080496219-76b9e6909700?w=600" + }, + { + "Id": "item_demo_spring_rolls", + "CategoryId": "cat_demo_food", + "Name": "\u0627\u0633\u067E\u0631\u06CC\u0646\u06AF \u0631\u0648\u0644", + "NameEn": "Spring rolls", + "NameAr": "\u0633\u0628\u0631\u064A\u0646\u063A \u0631\u0648\u0644", + "Description": null, + "priceToman": 115000, + "DiscountPercent": 0, + "Food101Class": "spring_rolls", + "imageUrl": "https://images.unsplash.com/photo-1526318896985-4d29c0903299?w=600" + }, + { + "Id": "item_demo_ramen", + "CategoryId": "cat_demo_food", + "Name": "\u0631\u0627\u0645\u0646", + "NameEn": "Ramen", + "NameAr": "\u0631\u0627\u0645\u0646", + "Description": null, + "priceToman": 235000, + "DiscountPercent": 0, + "Food101Class": "ramen", + "imageUrl": "https://images.unsplash.com/photo-1569718212165-3a8278d5f624?w=600" + }, + { + "Id": "item_demo_pho", + "CategoryId": "cat_demo_food", + "Name": "\u0641\u0648", + "NameEn": "Pho", + "NameAr": "\u0641\u0648", + "Description": null, + "priceToman": 225000, + "DiscountPercent": 0, + "Food101Class": "pho", + "imageUrl": "https://images.unsplash.com/photo-1591814468924-caf36d123dd6?w=600" + }, + { + "Id": "item_demo_sushi", + "CategoryId": "cat_demo_food", + "Name": "\u0633\u0648\u0634\u06CC", + "NameEn": "Sushi", + "NameAr": "\u0633\u0648\u0634\u064A", + "Description": "\u06F8 \u062A\u06A9\u0647", + "priceToman": 290000, + "DiscountPercent": 0, + "Food101Class": "sushi", + "imageUrl": "https://images.unsplash.com/photo-1579584425555-c3ce17fd4351?w=600" + }, + { + "Id": "item_demo_pasta", + "CategoryId": "cat_demo_pasta", + "Name": "\u067E\u0627\u0633\u062A\u0627 \u0622\u0644\u0641\u0631\u062F\u0648", + "NameEn": "Alfredo pasta", + "NameAr": "\u0628\u0627\u0633\u062A\u0627", + "Description": null, + "priceToman": 220000, + "DiscountPercent": 0, + "Food101Class": "pasta_carbonara", + "imageUrl": "https://images.unsplash.com/photo-1621996346565-e3dbc646d9a9?w=600" + }, + { + "Id": "item_demo_carbonara", + "CategoryId": "cat_demo_pasta", + "Name": "\u06A9\u0627\u0631\u0628\u0648\u0646\u0627\u0631\u0627", + "NameEn": "Carbonara", + "NameAr": "\u0643\u0627\u0631\u0628\u0648\u0646\u0627\u0631\u0627", + "Description": null, + "priceToman": 228000, + "DiscountPercent": 0, + "Food101Class": "spaghetti_carbonara", + "imageUrl": "https://images.unsplash.com/photo-1612874741227-866d1aeeecd1?w=600" + }, + { + "Id": "item_demo_bolognese", + "CategoryId": "cat_demo_pasta", + "Name": "\u0628\u0648\u0644\u0648\u0646\u0632", + "NameEn": "Bolognese", + "NameAr": "\u0628\u0648\u0644\u0648\u0646\u064A\u0632", + "Description": null, + "priceToman": 215000, + "DiscountPercent": 0, + "Food101Class": "spaghetti_bolognese", + "imageUrl": "https://images.unsplash.com/photo-1622973536968-77544a8a4e0e?w=600" + }, + { + "Id": "item_demo_lasagna", + "CategoryId": "cat_demo_pasta", + "Name": "\u0644\u0627\u0632\u0627\u0646\u06CC\u0627", + "NameEn": "Lasagna", + "NameAr": "\u0644\u0627\u0632\u0627\u0646\u064A\u0627", + "Description": null, + "priceToman": 240000, + "DiscountPercent": 0, + "Food101Class": "lasagna", + "imageUrl": "https://images.unsplash.com/photo-1574894709920-11b28e7367e3?w=600" + }, + { + "Id": "item_demo_gnocchi", + "CategoryId": "cat_demo_pasta", + "Name": "\u0646\u06CC\u0648\u06A9\u06CC", + "NameEn": "Gnocchi", + "NameAr": "\u062C\u0646\u0648\u0643\u064A", + "Description": null, + "priceToman": 232000, + "DiscountPercent": 0, + "Food101Class": "gnocchi", + "imageUrl": "https://images.unsplash.com/photo-1551183053-bf33a48c970a?w=600" + }, + { + "Id": "item_demo_pizza", + "CategoryId": "cat_demo_pasta", + "Name": "\u067E\u06CC\u062A\u0632\u0627 \u0645\u0627\u0631\u06AF\u0627\u0631\u06CC\u062A\u0627", + "NameEn": "Margherita pizza", + "NameAr": "\u0628\u064A\u062A\u0632\u0627", + "Description": null, + "priceToman": 265000, + "DiscountPercent": 10, + "Food101Class": "pizza", + "imageUrl": "https://images.unsplash.com/photo-1513104890138-7c749659a591?w=600" + }, + { + "Id": "item_demo_risotto", + "CategoryId": "cat_demo_pasta", + "Name": "\u0631\u06CC\u0632\u0648\u062A\u0648 \u0642\u0627\u0631\u0686", + "NameEn": "Mushroom risotto", + "NameAr": "\u0631\u064A\u0632\u0648\u062A\u0648", + "Description": null, + "priceToman": 238000, + "DiscountPercent": 0, + "Food101Class": "mushroom_risotto", + "imageUrl": "https://images.unsplash.com/photo-1476124362071-b9f2c96ef2db?w=600" + }, + { + "Id": "item_demo_cake", + "CategoryId": "cat_demo_dessert", + "Name": "\u06A9\u06CC\u06A9 \u0634\u06A9\u0644\u0627\u062A\u06CC", + "NameEn": "Chocolate cake", + "NameAr": "\u0643\u064A\u0643 \u0634\u0648\u0643\u0648\u0644\u0627\u062A\u0629", + "Description": "\u0628\u0631\u0634\u06CC", + "priceToman": 95000, + "DiscountPercent": 15, + "Food101Class": "chocolate_cake", + "imageUrl": "https://images.unsplash.com/photo-1578985545062-69928b1d9587?w=600" + }, + { + "Id": "item_demo_cheesecake", + "CategoryId": "cat_demo_dessert", + "Name": "\u0686\u06CC\u0632\u06A9\u06CC\u06A9", + "NameEn": "Cheesecake", + "NameAr": "\u062A\u0634\u064A\u0632 \u0643\u064A\u0643", + "Description": null, + "priceToman": 115000, + "DiscountPercent": 0, + "Food101Class": "cheesecake", + "imageUrl": "https://images.unsplash.com/photo-1524351199678-941a58cfcc36?w=600" + }, + { + "Id": "item_demo_brownie", + "CategoryId": "cat_demo_dessert", + "Name": "\u0628\u0631\u0627\u0648\u0646\u06CC", + "NameEn": "Brownie", + "NameAr": "\u0628\u0631\u0627\u0648\u0646\u064A", + "Description": "\u0628\u0633\u062A\u0646\u06CC \u0648\u0627\u0646\u06CC\u0644\u06CC", + "priceToman": 105000, + "DiscountPercent": 0, + "Food101Class": "brownie", + "imageUrl": "https://images.unsplash.com/photo-1606313564200-e75d5e30476e?w=600" + }, + { + "Id": "item_demo_icecream", + "CategoryId": "cat_demo_dessert", + "Name": "\u0628\u0633\u062A\u0646\u06CC", + "NameEn": "Ice cream", + "NameAr": "\u0622\u064A\u0633 \u0643\u0631\u064A\u0645", + "Description": "\u062F\u0648 \u0627\u0633\u06A9\u0648\u067E", + "priceToman": 88000, + "DiscountPercent": 0, + "Food101Class": "ice_cream", + "imageUrl": "https://images.unsplash.com/photo-1563805042-7684c019e1cb?w=600" + }, + { + "Id": "item_demo_tiramisu", + "CategoryId": "cat_demo_dessert", + "Name": "\u062A\u06CC\u0631\u0627\u0645\u06CC\u0633\u0648", + "NameEn": "Tiramisu", + "NameAr": "\u062A\u064A\u0631\u0627\u0645\u064A\u0633\u0648", + "Description": null, + "priceToman": 125000, + "DiscountPercent": 0, + "Food101Class": "tiramisu", + "imageUrl": "https://images.unsplash.com/photo-1571877227200-a0d98ea607e9?w=600" + }, + { + "Id": "item_demo_donuts", + "CategoryId": "cat_demo_dessert", + "Name": "\u062F\u0648\u0646\u0627\u062A", + "NameEn": "Donuts", + "NameAr": "\u062F\u0648\u0646\u0627\u062A", + "Description": null, + "priceToman": 78000, + "DiscountPercent": 0, + "Food101Class": "donuts", + "imageUrl": "https://images.unsplash.com/photo-1551024506-0bccd28d3071?w=600" + }, + { + "Id": "item_demo_churros", + "CategoryId": "cat_demo_dessert", + "Name": "\u0686\u0648\u0631\u0648\u0633", + "NameEn": "Churros", + "NameAr": "\u062A\u0634\u0648\u0631\u0648", + "Description": "\u0634\u06A9\u0644\u0627\u062A", + "priceToman": 92000, + "DiscountPercent": 0, + "Food101Class": "churros", + "imageUrl": "https://images.unsplash.com/photo-1627482299165-0883a056a48f?w=600" + }, + { + "Id": "item_demo_baklava", + "CategoryId": "cat_demo_dessert", + "Name": "\u0628\u0627\u0642\u0644\u0648\u0627", + "NameEn": "Baklava", + "NameAr": "\u0628\u0642\u0644\u0627\u0648\u0629", + "Description": null, + "priceToman": 98000, + "DiscountPercent": 0, + "Food101Class": "baklava", + "imageUrl": "https://images.unsplash.com/photo-1598110756419-84b30681f41f?w=600" + }, + { + "Id": "item_demo_creme_brulee", + "CategoryId": "cat_demo_dessert", + "Name": "\u06A9\u0631\u0645 \u0628\u0631\u0648\u0644\u0647", + "NameEn": "Cr\u00E8me br\u00FBl\u00E9e", + "NameAr": "\u0643\u0631\u064A\u0645 \u0628\u0631\u0648\u0644\u064A\u0647", + "Description": null, + "priceToman": 118000, + "DiscountPercent": 0, + "Food101Class": "creme_brulee", + "imageUrl": "https://images.unsplash.com/photo-1470309864661-683be0ef7eaf?w=600" + } + ] +} diff --git a/data/menu-image-manifest.json b/data/menu-image-manifest.json new file mode 100644 index 0000000..6919f42 --- /dev/null +++ b/data/menu-image-manifest.json @@ -0,0 +1,298 @@ +{ + "version": 2, + "source": "Food-101 class mapping (Unsplash fallbacks until Kaggle JPEG import)", + "defaults": { + "drink": "https://images.unsplash.com/photo-1509042239860-f550ce710b93?w=600\u0026auto=format\u0026fit=crop", + "food": "https://images.unsplash.com/photo-1546793665-c74683f339c1?w=600" + }, + "items": { + "item_demo_espresso": { + "food101Class": "espresso", + "imageUrl": "https://images.unsplash.com/photo-1509042239860-f550ce710b93?w=600\u0026auto=format\u0026fit=crop", + "nameEn": "Espresso", + "categoryId": "cat_demo_drinks" + }, + "item_demo_americano": { + "food101Class": "cappuccino", + "imageUrl": "https://images.unsplash.com/photo-1572442388796-11668a67e3d0?w=600", + "nameEn": "Americano", + "categoryId": "cat_demo_drinks" + }, + "item_demo_latte": { + "food101Class": "latte", + "imageUrl": "https://images.unsplash.com/photo-1461023058943-07fcbe16d735?w=600", + "nameEn": "Latte", + "categoryId": "cat_demo_drinks" + }, + "item_demo_cappuccino": { + "food101Class": "cappuccino", + "imageUrl": "https://images.unsplash.com/photo-1572442388796-11668a67e3d0?w=600", + "nameEn": "Cappuccino", + "categoryId": "cat_demo_drinks" + }, + "item_demo_mocha": { + "food101Class": "mocha", + "imageUrl": "https://images.unsplash.com/photo-1577887233537-a81b387b125e?w=600", + "nameEn": "Mocha", + "categoryId": "cat_demo_drinks" + }, + "item_demo_tea": { + "food101Class": "miso_soup", + "imageUrl": "https://images.unsplash.com/photo-1556679343-c7306c1976bc?w=600", + "nameEn": "Masala tea", + "categoryId": "cat_demo_drinks" + }, + "item_demo_iced_latte": { + "food101Class": "iced_coffee", + "imageUrl": "https://images.unsplash.com/photo-1517487881594-2787aeee8f58?w=600\u0026auto=format\u0026fit=crop", + "nameEn": "Iced latte", + "categoryId": "cat_demo_cold" + }, + "item_demo_cold_brew": { + "food101Class": "iced_coffee", + "imageUrl": "https://images.unsplash.com/photo-1517487881594-2787aeee8f58?w=600\u0026auto=format\u0026fit=crop", + "nameEn": "Cold brew", + "categoryId": "cat_demo_cold" + }, + "item_demo_lemonade": { + "food101Class": "lemonade", + "imageUrl": "https://images.unsplash.com/photo-1523672990561-64c16245f769?w=600", + "nameEn": "Lemonade", + "categoryId": "cat_demo_cold" + }, + "item_demo_smoothie": { + "food101Class": "smoothie", + "imageUrl": "https://images.unsplash.com/photo-1505252585463-0433371f7f6b?w=600", + "nameEn": "Berry smoothie", + "categoryId": "cat_demo_cold" + }, + "item_demo_croissant": { + "food101Class": "croque_madame", + "imageUrl": "https://images.unsplash.com/photo-1555507036342-9231d37c10f3?w=600", + "nameEn": "Croissant", + "categoryId": "cat_demo_breakfast" + }, + "item_demo_omelette": { + "food101Class": "omelette", + "imageUrl": "https://images.unsplash.com/photo-1525351484343-752d43d363f1?w=600", + "nameEn": "Omelette", + "categoryId": "cat_demo_breakfast" + }, + "item_demo_avocado": { + "food101Class": "avocado_toast", + "imageUrl": "https://images.unsplash.com/photo-1541519221064-49632fb3380e?w=600", + "nameEn": "Avocado toast", + "categoryId": "cat_demo_breakfast" + }, + "item_demo_pancakes": { + "food101Class": "pancakes", + "imageUrl": "https://images.unsplash.com/photo-1567620905732-2d1ec7ab7440?w=600", + "nameEn": "Pancakes", + "categoryId": "cat_demo_breakfast" + }, + "item_demo_waffles": { + "food101Class": "waffles", + "imageUrl": "https://images.unsplash.com/photo-1567818735240-7acbb4b7e34c?w=600", + "nameEn": "Waffles", + "categoryId": "cat_demo_breakfast" + }, + "item_demo_french_toast": { + "food101Class": "french_toast", + "imageUrl": "https://images.unsplash.com/photo-1484723091739-30a329e1f0c4?w=600", + "nameEn": "French toast", + "categoryId": "cat_demo_breakfast" + }, + "item_demo_eggs_benedict": { + "food101Class": "eggs_benedict", + "imageUrl": "https://images.unsplash.com/photo-1608039819502-3d5a2a2e0b0e?w=600", + "nameEn": "Eggs Benedict", + "categoryId": "cat_demo_breakfast" + }, + "item_demo_sandwich": { + "food101Class": "club_sandwich", + "imageUrl": "https://images.unsplash.com/photo-1528735602780-2552fd46c7af?w=600", + "nameEn": "Chicken sandwich", + "categoryId": "cat_demo_food" + }, + "item_demo_salad": { + "food101Class": "caesar_salad", + "imageUrl": "https://images.unsplash.com/photo-1546793665-c74683f339c1?w=600", + "nameEn": "Caesar salad", + "categoryId": "cat_demo_food" + }, + "item_demo_greek_salad": { + "food101Class": "greek_salad", + "imageUrl": "https://images.unsplash.com/photo-1540189549336-e6e99c3679fe?w=600", + "nameEn": "Greek salad", + "categoryId": "cat_demo_food" + }, + "item_demo_burger": { + "food101Class": "hamburger", + "imageUrl": "https://images.unsplash.com/photo-1568901346375-23c9450c58cd?w=600", + "nameEn": "Burger", + "categoryId": "cat_demo_food" + }, + "item_demo_steak": { + "food101Class": "steak", + "imageUrl": "https://images.unsplash.com/photo-1600891963295-d66a269b9202?w=600", + "nameEn": "Steak", + "categoryId": "cat_demo_food" + }, + "item_demo_salmon": { + "food101Class": "grilled_salmon", + "imageUrl": "https://images.unsplash.com/photo-1467003909585-2f8a72700288?w=600", + "nameEn": "Grilled salmon", + "categoryId": "cat_demo_food" + }, + "item_demo_tacos": { + "food101Class": "tacos", + "imageUrl": "https://images.unsplash.com/photo-1565299585323-38174c4aab1e?w=600", + "nameEn": "Tacos", + "categoryId": "cat_demo_food" + }, + "item_demo_shawarma": { + "food101Class": "shawarma", + "imageUrl": "https://images.unsplash.com/photo-1529006557810-274adbcb39d8?w=600", + "nameEn": "Shawarma", + "categoryId": "cat_demo_food" + }, + "item_demo_falafel": { + "food101Class": "falafel", + "imageUrl": "https://images.unsplash.com/photo-1601050690597-df5748fb5cee?w=600", + "nameEn": "Falafel", + "categoryId": "cat_demo_food" + }, + "item_demo_hummus": { + "food101Class": "hummus", + "imageUrl": "https://images.unsplash.com/photo-1626208082043-e6319abfeec2?w=600", + "nameEn": "Hummus", + "categoryId": "cat_demo_food" + }, + "item_demo_fries": { + "food101Class": "french_fries", + "imageUrl": "https://images.unsplash.com/photo-1573080496219-76b9e6909700?w=600", + "nameEn": "French fries", + "categoryId": "cat_demo_food" + }, + "item_demo_spring_rolls": { + "food101Class": "spring_rolls", + "imageUrl": "https://images.unsplash.com/photo-1526318896985-4d29c0903299?w=600", + "nameEn": "Spring rolls", + "categoryId": "cat_demo_food" + }, + "item_demo_ramen": { + "food101Class": "ramen", + "imageUrl": "https://images.unsplash.com/photo-1569718212165-3a8278d5f624?w=600", + "nameEn": "Ramen", + "categoryId": "cat_demo_food" + }, + "item_demo_pho": { + "food101Class": "pho", + "imageUrl": "https://images.unsplash.com/photo-1591814468924-caf36d123dd6?w=600", + "nameEn": "Pho", + "categoryId": "cat_demo_food" + }, + "item_demo_sushi": { + "food101Class": "sushi", + "imageUrl": "https://images.unsplash.com/photo-1579584425555-c3ce17fd4351?w=600", + "nameEn": "Sushi", + "categoryId": "cat_demo_food" + }, + "item_demo_pasta": { + "food101Class": "pasta_carbonara", + "imageUrl": "https://images.unsplash.com/photo-1621996346565-e3dbc646d9a9?w=600", + "nameEn": "Alfredo pasta", + "categoryId": "cat_demo_pasta" + }, + "item_demo_carbonara": { + "food101Class": "spaghetti_carbonara", + "imageUrl": "https://images.unsplash.com/photo-1612874741227-866d1aeeecd1?w=600", + "nameEn": "Carbonara", + "categoryId": "cat_demo_pasta" + }, + "item_demo_bolognese": { + "food101Class": "spaghetti_bolognese", + "imageUrl": "https://images.unsplash.com/photo-1622973536968-77544a8a4e0e?w=600", + "nameEn": "Bolognese", + "categoryId": "cat_demo_pasta" + }, + "item_demo_lasagna": { + "food101Class": "lasagna", + "imageUrl": "https://images.unsplash.com/photo-1574894709920-11b28e7367e3?w=600", + "nameEn": "Lasagna", + "categoryId": "cat_demo_pasta" + }, + "item_demo_gnocchi": { + "food101Class": "gnocchi", + "imageUrl": "https://images.unsplash.com/photo-1551183053-bf33a48c970a?w=600", + "nameEn": "Gnocchi", + "categoryId": "cat_demo_pasta" + }, + "item_demo_pizza": { + "food101Class": "pizza", + "imageUrl": "https://images.unsplash.com/photo-1513104890138-7c749659a591?w=600", + "nameEn": "Margherita pizza", + "categoryId": "cat_demo_pasta" + }, + "item_demo_risotto": { + "food101Class": "mushroom_risotto", + "imageUrl": "https://images.unsplash.com/photo-1476124362071-b9f2c96ef2db?w=600", + "nameEn": "Mushroom risotto", + "categoryId": "cat_demo_pasta" + }, + "item_demo_cake": { + "food101Class": "chocolate_cake", + "imageUrl": "https://images.unsplash.com/photo-1578985545062-69928b1d9587?w=600", + "nameEn": "Chocolate cake", + "categoryId": "cat_demo_dessert" + }, + "item_demo_cheesecake": { + "food101Class": "cheesecake", + "imageUrl": "https://images.unsplash.com/photo-1524351199678-941a58cfcc36?w=600", + "nameEn": "Cheesecake", + "categoryId": "cat_demo_dessert" + }, + "item_demo_brownie": { + "food101Class": "brownie", + "imageUrl": "https://images.unsplash.com/photo-1606313564200-e75d5e30476e?w=600", + "nameEn": "Brownie", + "categoryId": "cat_demo_dessert" + }, + "item_demo_icecream": { + "food101Class": "ice_cream", + "imageUrl": "https://images.unsplash.com/photo-1563805042-7684c019e1cb?w=600", + "nameEn": "Ice cream", + "categoryId": "cat_demo_dessert" + }, + "item_demo_tiramisu": { + "food101Class": "tiramisu", + "imageUrl": "https://images.unsplash.com/photo-1571877227200-a0d98ea607e9?w=600", + "nameEn": "Tiramisu", + "categoryId": "cat_demo_dessert" + }, + "item_demo_donuts": { + "food101Class": "donuts", + "imageUrl": "https://images.unsplash.com/photo-1551024506-0bccd28d3071?w=600", + "nameEn": "Donuts", + "categoryId": "cat_demo_dessert" + }, + "item_demo_churros": { + "food101Class": "churros", + "imageUrl": "https://images.unsplash.com/photo-1627482299165-0883a056a48f?w=600", + "nameEn": "Churros", + "categoryId": "cat_demo_dessert" + }, + "item_demo_baklava": { + "food101Class": "baklava", + "imageUrl": "https://images.unsplash.com/photo-1598110756419-84b30681f41f?w=600", + "nameEn": "Baklava", + "categoryId": "cat_demo_dessert" + }, + "item_demo_creme_brulee": { + "food101Class": "creme_brulee", + "imageUrl": "https://images.unsplash.com/photo-1470309864661-683be0ef7eaf?w=600", + "nameEn": "Cr\u00E8me br\u00FBl\u00E9e", + "categoryId": "cat_demo_dessert" + } + } +} diff --git a/src/Meezi.API/16 b/src/Meezi.API/16 new file mode 100644 index 0000000..2c477b6 Binary files /dev/null and b/src/Meezi.API/16 differ diff --git a/src/Meezi.API/Configuration/DeliveryPlatformsOptions.cs b/src/Meezi.API/Configuration/DeliveryPlatformsOptions.cs new file mode 100644 index 0000000..e2ba572 --- /dev/null +++ b/src/Meezi.API/Configuration/DeliveryPlatformsOptions.cs @@ -0,0 +1,34 @@ +namespace Meezi.API.Configuration; + +public class DeliveryPlatformsOptions +{ + public const string SectionName = "DeliveryPlatforms"; + + public SnappfoodPlatformOptions Snappfood { get; set; } = new(); + public Tap30PlatformOptions Tap30 { get; set; } = new(); + public DigikalaPlatformOptions Digikala { get; set; } = new(); + + public decimal DefaultSnappfoodCommissionPercent { get; set; } = 18m; + public decimal DefaultTap30CommissionPercent { get; set; } = 15m; + public decimal DefaultDigikalaCommissionPercent { get; set; } = 12m; +} + +public class SnappfoodPlatformOptions +{ + public string WebhookSecret { get; set; } = ""; + public string ApiKey { get; set; } = ""; + public string ApiBaseUrl { get; set; } = ""; +} + +public class Tap30PlatformOptions +{ + public string WebhookSecret { get; set; } = ""; + public string ApiKey { get; set; } = ""; + public string ApiBaseUrl { get; set; } = ""; +} + +public class DigikalaPlatformOptions +{ + public string WebhookSecret { get; set; } = ""; + public bool Enabled { get; set; } +} diff --git a/src/Meezi.API/Configuration/MenuAi3dOptions.cs b/src/Meezi.API/Configuration/MenuAi3dOptions.cs new file mode 100644 index 0000000..853c0b4 --- /dev/null +++ b/src/Meezi.API/Configuration/MenuAi3dOptions.cs @@ -0,0 +1,14 @@ +namespace Meezi.API.Configuration; + +public class MenuAi3dOptions +{ + public const string SectionName = "Ai3d"; + + public string ApiKey { get; set; } = ""; + public string BaseUrl { get; set; } = "https://api.meshy.ai"; + public string ImageTo3dPath { get; set; } = "/openapi/v1/image-to-3d"; + public int PollIntervalSeconds { get; set; } = 4; + public int PollTimeoutSeconds { get; set; } = 300; + /// When true and ApiKey is empty, returns a minimal GLB (development only). + public bool AllowDevStub { get; set; } +} diff --git a/src/Meezi.API/Controllers/.gitkeep b/src/Meezi.API/Controllers/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/Meezi.API/Controllers/.gitkeep @@ -0,0 +1 @@ + diff --git a/src/Meezi.API/Controllers/AuthController.cs b/src/Meezi.API/Controllers/AuthController.cs new file mode 100644 index 0000000..72a16a8 --- /dev/null +++ b/src/Meezi.API/Controllers/AuthController.cs @@ -0,0 +1,125 @@ +using FluentValidation; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.RateLimiting; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using Meezi.API.Models.Auth; +using Meezi.API.Services; +using Meezi.Core.Constants; +using Meezi.Shared; + +namespace Meezi.API.Controllers; + +[ApiController] +[Route("api/auth")] +public class AuthController : ControllerBase +{ + private readonly IAuthService _authService; + private readonly IValidator _sendOtpValidator; + private readonly IValidator _verifyOtpValidator; + private readonly IValidator _refreshValidator; + + public AuthController( + IAuthService authService, + IValidator sendOtpValidator, + IValidator verifyOtpValidator, + IValidator refreshValidator) + { + _authService = authService; + _sendOtpValidator = sendOtpValidator; + _verifyOtpValidator = verifyOtpValidator; + _refreshValidator = refreshValidator; + } + + [HttpPost("send-otp")] + [EnableRateLimiting("auth-otp")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task SendOtp([FromBody] SendOtpRequest request, CancellationToken cancellationToken) + { + var validation = await _sendOtpValidator.ValidateAsync(request, cancellationToken); + if (!validation.IsValid) + return BadRequest(ValidationError(validation)); + + var (success, data, code, message) = await _authService.SendOtpAsync(request, cancellationToken); + if (!success) + return ErrorResult(code!, message!); + + return Ok(new ApiResponse(true, data)); + } + + [HttpPost("verify-otp")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task VerifyOtp([FromBody] VerifyOtpRequest request, CancellationToken cancellationToken) + { + var validation = await _verifyOtpValidator.ValidateAsync(request, cancellationToken); + if (!validation.IsValid) + return BadRequest(ValidationError(validation)); + + var (success, data, code, message) = await _authService.VerifyOtpAsync(request, cancellationToken); + if (!success) + return ErrorResult(code!, message!); + + return Ok(new ApiResponse(true, data)); + } + + [HttpPost("refresh")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task Refresh([FromBody] RefreshTokenRequest request, CancellationToken cancellationToken) + { + var validation = await _refreshValidator.ValidateAsync(request, cancellationToken); + if (!validation.IsValid) + return BadRequest(ValidationError(validation)); + + var (success, data, code, message) = await _authService.RefreshAsync(request, cancellationToken); + if (!success) + return ErrorResult(code!, message!); + + return Ok(new ApiResponse(true, data)); + } + + [HttpGet("me")] + [Authorize] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public IActionResult GetMe() + { + var userId = User.FindFirstValue(JwtRegisteredClaimNames.Sub) + ?? User.FindFirstValue(ClaimTypes.NameIdentifier) + ?? string.Empty; + + var expClaim = User.FindFirstValue(JwtRegisteredClaimNames.Exp); + var expiresAt = expClaim != null && long.TryParse(expClaim, out var exp) + ? DateTimeOffset.FromUnixTimeSeconds(exp).UtcDateTime + : DateTime.UtcNow; + + var data = new AuthTokenResponse( + AccessToken: string.Empty, + RefreshToken: string.Empty, + ExpiresAt: expiresAt, + UserId: userId, + CafeId: User.FindFirstValue(MeeziClaimTypes.CafeId) ?? string.Empty, + Role: User.FindFirstValue(MeeziClaimTypes.Role) ?? string.Empty, + PlanTier: User.FindFirstValue(MeeziClaimTypes.PlanTier) ?? string.Empty, + Language: User.FindFirstValue(MeeziClaimTypes.Language) ?? string.Empty, + Actor: User.FindFirstValue(MeeziClaimTypes.Actor) ?? MeeziActorKinds.Merchant, + BranchId: User.FindFirstValue(MeeziClaimTypes.BranchId)); + + return Ok(new ApiResponse(true, data)); + } + + private static ApiResponse ValidationError(FluentValidation.Results.ValidationResult validation) + { + var first = validation.Errors.First(); + return new ApiResponse(false, null, new ApiError("VALIDATION_ERROR", first.ErrorMessage, first.PropertyName)); + } + + private IActionResult ErrorResult(string code, string message) => code switch + { + "RATE_LIMITED" => StatusCode(StatusCodes.Status429TooManyRequests, + new ApiResponse(false, null, new ApiError(code, message))), + "NOT_FOUND" => NotFound(new ApiResponse(false, null, new ApiError(code, message))), + "INVALID_OTP" or "INVALID_TOKEN" => Unauthorized(new ApiResponse(false, null, new ApiError(code, message))), + _ => BadRequest(new ApiResponse(false, null, new ApiError(code, message))) + }; +} diff --git a/src/Meezi.API/Controllers/BillingController.cs b/src/Meezi.API/Controllers/BillingController.cs new file mode 100644 index 0000000..250173d --- /dev/null +++ b/src/Meezi.API/Controllers/BillingController.cs @@ -0,0 +1,106 @@ +using FluentValidation; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Meezi.API.Models.Billing; +using Meezi.API.Services; +using Meezi.Core.Interfaces; +using Meezi.Shared; + +namespace Meezi.API.Controllers; + +[ApiController] +public class BillingController : ControllerBase +{ + private readonly IBillingService _billing; + private readonly IValidator _subscribeValidator; + + public BillingController(IBillingService billing, IValidator subscribeValidator) + { + _billing = billing; + _subscribeValidator = subscribeValidator; + } + + [Authorize] + [HttpPost("api/billing/subscribe")] + public async Task Subscribe( + [FromBody] SubscribeRequest request, + ITenantContext tenant, + CancellationToken ct) + { + if (string.IsNullOrEmpty(tenant.CafeId)) + return Unauthorized(); + if (tenant.Role != Core.Enums.EmployeeRole.Owner) + { + return StatusCode(403, new ApiResponse(false, null, + new ApiError("OWNER_REQUIRED", "Only the cafe owner can manage subscription billing."))); + } + + var validation = await _subscribeValidator.ValidateAsync(request, ct); + if (!validation.IsValid) + { + var first = validation.Errors.First(); + return BadRequest(new ApiResponse(false, null, new ApiError("VALIDATION_ERROR", first.ErrorMessage, first.PropertyName))); + } + + var (data, code, message) = await _billing.InitiateSubscriptionAsync(tenant.CafeId, request, ct); + if (data is null) + return BadRequest(new ApiResponse(false, null, new ApiError(code ?? "ERROR", message ?? "Failed."))); + + return Ok(new ApiResponse(true, data)); + } + + [AllowAnonymous] + [HttpGet("api/billing/verify")] + public async Task Verify( + [FromQuery] string Authority, + [FromQuery] string? Status, + CancellationToken ct) + { + var result = await _billing.VerifyZarinPalAsync(Authority, Status, ct); + return Redirect(result.RedirectUrl); + } + + [AllowAnonymous] + [HttpGet("api/billing/verify/snapppay")] + public async Task VerifySnappPay( + [FromQuery] string? paymentToken, + [FromQuery] string? state, + CancellationToken ct) + { + var result = await _billing.VerifySnappPayAsync(paymentToken, state, ct); + return Redirect(result.RedirectUrl); + } + + [AllowAnonymous] + [HttpGet("api/billing/verify/tara")] + public async Task VerifyTara( + [FromQuery] string? traceNumber, + [FromQuery] string? status, + CancellationToken ct) + { + var result = await _billing.VerifyTaraAsync(traceNumber, status, ct); + return Redirect(result.RedirectUrl); + } + + [Authorize] + [HttpGet("api/billing/payment-methods")] + public async Task PaymentMethods(CancellationToken ct) + { + var methods = await _billing.GetPaymentMethodsAsync(ct); + return Ok(new ApiResponse>(true, methods)); + } + + [Authorize] + [HttpGet("api/billing/status")] + public async Task Status(ITenantContext tenant, CancellationToken ct) + { + if (string.IsNullOrEmpty(tenant.CafeId) || tenant.PlanTier is null) + return Unauthorized(); + + var data = await _billing.GetStatusAsync(tenant.CafeId, tenant.PlanTier.Value, ct); + if (data is null) + return NotFound(new ApiResponse(false, null, new ApiError("NOT_FOUND", "Cafe not found."))); + + return Ok(new ApiResponse(true, data)); + } +} diff --git a/src/Meezi.API/Controllers/BranchMenuController.cs b/src/Meezi.API/Controllers/BranchMenuController.cs new file mode 100644 index 0000000..7a06c50 --- /dev/null +++ b/src/Meezi.API/Controllers/BranchMenuController.cs @@ -0,0 +1,104 @@ +using FluentValidation; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Meezi.API.Models.Menu; +using Meezi.API.Services; +using Meezi.Core.Enums; +using Meezi.Core.Interfaces; +using Meezi.Shared; + +namespace Meezi.API.Controllers; + +[Route("api/cafes/{cafeId}/branches/{branchId}/menu")] +public class BranchMenuController : CafeApiControllerBase +{ + private readonly IBranchMenuService _branchMenu; + private readonly IValidator _upsertValidator; + + public BranchMenuController( + IBranchMenuService branchMenu, + IValidator upsertValidator) + { + _branchMenu = branchMenu; + _upsertValidator = upsertValidator; + } + + [HttpGet] + public async Task GetBranchMenu( + string cafeId, + string branchId, + ITenantContext tenant, + CancellationToken cancellationToken, + [FromQuery] bool includeUnavailable = false) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + + var data = await _branchMenu.GetBranchMenuAsync( + cafeId, branchId, includeUnavailable, cancellationToken); + if (data is null) + return NotFound(new ApiResponse( + false, null, new ApiError("BRANCH_NOT_FOUND", "Branch not found."))); + + return Ok(new ApiResponse>(true, data)); + } + + [HttpPut("{menuItemId}/override")] + [Authorize(Roles = "Manager,Owner")] + public async Task UpsertOverride( + string cafeId, + string branchId, + string menuItemId, + [FromBody] UpsertBranchMenuOverrideRequest request, + ITenantContext tenant, + CancellationToken cancellationToken = default) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (!BranchMenuService.CanManageOverrides(tenant.Role)) + return Forbid(); + + var validation = await _upsertValidator.ValidateAsync(request, cancellationToken); + if (!validation.IsValid) return BadRequest(ValidationError(validation)); + + var plan = tenant.PlanTier ?? PlanTier.Free; + var result = await _branchMenu.UpsertOverrideAsync( + cafeId, + branchId, + menuItemId, + request, + plan, + tenant.Role, + tenant.UserId, + cancellationToken); + + if (!result.Success) + { + var status = result.ErrorCode == "PLAN_LIMIT_REACHED" + ? StatusCodes.Status403Forbidden + : StatusCodes.Status400BadRequest; + return StatusCode(status, + new ApiResponse(false, null, + new ApiError(result.ErrorCode!, result.Message!))); + } + + return Ok(new ApiResponse(true, result.Data)); + } + + [HttpDelete("{menuItemId}/override")] + [Authorize(Roles = "Owner")] + public async Task DeleteOverride( + string cafeId, + string branchId, + string menuItemId, + ITenantContext tenant, + CancellationToken cancellationToken = default) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + + var deleted = await _branchMenu.DeleteOverrideAsync( + cafeId, branchId, menuItemId, cancellationToken); + if (!deleted) + return NotFoundError("Override not found."); + + return Ok(new ApiResponse(true, new { menuItemId })); + } +} diff --git a/src/Meezi.API/Controllers/BranchPrintSettingsController.cs b/src/Meezi.API/Controllers/BranchPrintSettingsController.cs new file mode 100644 index 0000000..1025df1 --- /dev/null +++ b/src/Meezi.API/Controllers/BranchPrintSettingsController.cs @@ -0,0 +1,114 @@ +using FluentValidation; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Meezi.API.Models.Printing; +using Meezi.Core.Entities; +using Meezi.Core.Interfaces; +using Meezi.Infrastructure.Data; +using Meezi.Shared; +using Microsoft.EntityFrameworkCore; + +namespace Meezi.API.Controllers; + +[Route("api/cafes/{cafeId}/branches/{branchId}/print-settings")] +[Authorize(Roles = "Manager,Owner")] +public class BranchPrintSettingsController : CafeApiControllerBase +{ + private readonly AppDbContext _db; + private readonly IValidator _validator; + + public BranchPrintSettingsController( + AppDbContext db, + IValidator validator) + { + _db = db; + _validator = validator; + } + + [HttpGet] + public async Task Get( + string cafeId, + string branchId, + ITenantContext tenant, + CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + + var branch = await _db.Branches + .AsNoTracking() + .FirstOrDefaultAsync(b => b.Id == branchId && b.CafeId == cafeId, ct); + + if (branch is null) + return NotFound(new ApiResponse(false, null, + new ApiError("BRANCH_NOT_FOUND", "Branch not found."))); + + return Ok(new ApiResponse(true, ToDto(branch))); + } + + [HttpPatch] + public async Task Patch( + string cafeId, + string branchId, + [FromBody] PatchBranchPrintSettingsRequest request, + ITenantContext tenant, + CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + + var validation = await _validator.ValidateAsync(request, ct); + if (!validation.IsValid) return BadRequest(ValidationError(validation)); + + var branch = await _db.Branches.FirstOrDefaultAsync(b => b.Id == branchId && b.CafeId == cafeId, ct); + if (branch is null) + return NotFound(new ApiResponse(false, null, + new ApiError("BRANCH_NOT_FOUND", "Branch not found."))); + + if (request.ReceiptPrinterIp is not null) + branch.ReceiptPrinterIp = string.IsNullOrWhiteSpace(request.ReceiptPrinterIp) + ? null + : request.ReceiptPrinterIp.Trim(); + if (request.ReceiptPrinterPort.HasValue) + branch.ReceiptPrinterPort = request.ReceiptPrinterPort.Value; + if (request.KitchenPrinterIp is not null) + branch.KitchenPrinterIp = string.IsNullOrWhiteSpace(request.KitchenPrinterIp) + ? null + : request.KitchenPrinterIp.Trim(); + if (request.KitchenPrinterPort.HasValue) + branch.KitchenPrinterPort = request.KitchenPrinterPort.Value; + if (request.PaperWidthMm.HasValue) + branch.PaperWidthMm = request.PaperWidthMm.Value is 58 or 80 ? request.PaperWidthMm.Value : 80; + if (request.AutoCutEnabled.HasValue) + branch.AutoCutEnabled = request.AutoCutEnabled.Value; + if (request.ReceiptHeader is not null) + branch.ReceiptHeader = string.IsNullOrWhiteSpace(request.ReceiptHeader) ? null : request.ReceiptHeader.Trim(); + if (request.ReceiptFooter is not null) + branch.ReceiptFooter = string.IsNullOrWhiteSpace(request.ReceiptFooter) ? null : request.ReceiptFooter.Trim(); + if (request.WifiPassword is not null) + branch.WifiPassword = string.IsNullOrWhiteSpace(request.WifiPassword) ? null : request.WifiPassword.Trim(); + if (request.PosDeviceIp is not null) + branch.PosDeviceIp = string.IsNullOrWhiteSpace(request.PosDeviceIp) + ? null + : request.PosDeviceIp.Trim(); + if (request.PosDevicePort.HasValue) + branch.PosDevicePort = request.PosDevicePort.Value; + + branch.UpdatedAt = DateTime.UtcNow; + await _db.SaveChangesAsync(ct); + + return Ok(new ApiResponse(true, ToDto(branch))); + } + + private static BranchPrintSettingsDto ToDto(Branch b) => new( + b.Id, + b.ReceiptPrinterIp, + b.ReceiptPrinterPort, + b.KitchenPrinterIp, + b.KitchenPrinterPort, + b.PaperWidthMm is 58 or 80 ? b.PaperWidthMm : 80, + b.AutoCutEnabled, + b.ReceiptHeader, + b.ReceiptFooter, + b.WifiPassword, + b.PosDeviceIp, + b.PosDevicePort); +} diff --git a/src/Meezi.API/Controllers/BranchTablesController.cs b/src/Meezi.API/Controllers/BranchTablesController.cs new file mode 100644 index 0000000..16d1583 --- /dev/null +++ b/src/Meezi.API/Controllers/BranchTablesController.cs @@ -0,0 +1,253 @@ +using FluentValidation; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Meezi.API.Models.Tables; +using Meezi.API.Services; +using Meezi.Core.Enums; +using Meezi.Core.Interfaces; +using Meezi.Shared; + +namespace Meezi.API.Controllers; + +[Route("api/cafes/{cafeId}/branches/{branchId}/tables")] +public class BranchTablesController : CafeApiControllerBase +{ + private readonly ITableService _tables; + private readonly IValidator _createTableValidator; + private readonly IValidator _patchTableValidator; + private readonly IValidator _createSectionValidator; + private readonly IValidator _patchSectionValidator; + private readonly IValidator _cleaningValidator; + + public BranchTablesController( + ITableService tables, + IValidator createTableValidator, + IValidator patchTableValidator, + IValidator createSectionValidator, + IValidator patchSectionValidator, + IValidator cleaningValidator) + { + _tables = tables; + _createTableValidator = createTableValidator; + _patchTableValidator = patchTableValidator; + _createSectionValidator = createSectionValidator; + _patchSectionValidator = patchSectionValidator; + _cleaningValidator = cleaningValidator; + } + + [HttpGet("board")] + public async Task GetBoard( + string cafeId, + string branchId, + ITenantContext tenant, + [FromQuery] bool activeOnly = true, + CancellationToken ct = default) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (!await _tables.CanAccessBranchAsync(cafeId, branchId, tenant.UserId, tenant.Role, ct)) + return Forbid(); + + var data = await _tables.GetBranchTableBoardAsync(cafeId, branchId, activeOnly, ct); + return Ok(new ApiResponse>(true, data)); + } + + [HttpGet] + public async Task GetTables( + string cafeId, + string branchId, + ITenantContext tenant, + CancellationToken ct = default) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (!await _tables.CanAccessBranchAsync(cafeId, branchId, tenant.UserId, tenant.Role, ct)) + return Forbid(); + + var data = await _tables.GetBranchTablesAsync(cafeId, branchId, ct); + if (data is null) return NotFoundError("Branch not found."); + return Ok(new ApiResponse>(true, data)); + } + + [HttpPost] + [Authorize(Roles = "Manager,Owner")] + public async Task CreateTable( + string cafeId, + string branchId, + [FromBody] CreateBranchTableRequest request, + ITenantContext tenant, + CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (!await _tables.CanAccessBranchAsync(cafeId, branchId, tenant.UserId, tenant.Role, ct)) + return Forbid(); + + var validation = await _createTableValidator.ValidateAsync(request, ct); + if (!validation.IsValid) return BadRequest(ValidationError(validation)); + + var result = await _tables.CreateBranchTableAsync(cafeId, branchId, request, ct); + return BranchOpResult(result); + } + + [HttpPatch("{id}")] + [Authorize(Roles = "Manager,Owner")] + public async Task PatchTable( + string cafeId, + string branchId, + string id, + [FromBody] PatchBranchTableRequest request, + ITenantContext tenant, + CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (!await _tables.CanAccessBranchAsync(cafeId, branchId, tenant.UserId, tenant.Role, ct)) + return Forbid(); + + var validation = await _patchTableValidator.ValidateAsync(request, ct); + if (!validation.IsValid) return BadRequest(ValidationError(validation)); + + var result = await _tables.PatchBranchTableAsync(cafeId, branchId, id, request, ct); + return BranchOpResult(result); + } + + [HttpDelete("{id}")] + [Authorize(Roles = "Manager,Owner")] + public async Task DeleteTable( + string cafeId, + string branchId, + string id, + ITenantContext tenant, + CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (!await _tables.CanAccessBranchAsync(cafeId, branchId, tenant.UserId, tenant.Role, ct)) + return Forbid(); + + var result = await _tables.DeleteBranchTableAsync(cafeId, branchId, id, ct); + return BranchOpResult(result); + } + + [HttpPatch("{id}/cleaning")] + public async Task SetCleaning( + string cafeId, + string branchId, + string id, + [FromBody] SetTableCleaningRequest request, + ITenantContext tenant, + CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (!await _tables.CanAccessBranchAsync(cafeId, branchId, tenant.UserId, tenant.Role, ct)) + return Forbid(); + + var validation = await _cleaningValidator.ValidateAsync(request, ct); + if (!validation.IsValid) return BadRequest(ValidationError(validation)); + + var data = await _tables.SetTableCleaningAsync(cafeId, id, request.IsCleaning, ct); + if (data is null || data.BranchId != branchId) return NotFoundError(); + return Ok(new ApiResponse(true, data)); + } + + [HttpGet("{id}/qr")] + public async Task GetQrPng( + string cafeId, + string branchId, + string id, + ITenantContext tenant, + CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (!await _tables.CanAccessBranchAsync(cafeId, branchId, tenant.UserId, tenant.Role, ct)) + return Forbid(); + + var png = await _tables.GetQrPngAsync(cafeId, id, ct); + if (png is null) return NotFoundError(); + return File(png, "image/png", $"table-{id}-qr.png"); + } + + [HttpGet("sections")] + public async Task GetSections( + string cafeId, + string branchId, + ITenantContext tenant, + CancellationToken ct = default) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (!await _tables.CanAccessBranchAsync(cafeId, branchId, tenant.UserId, tenant.Role, ct)) + return Forbid(); + + var data = await _tables.GetBranchSectionsAsync(cafeId, branchId, ct); + if (data is null) return NotFoundError("Branch not found."); + return Ok(new ApiResponse>(true, data)); + } + + [HttpPost("sections")] + [Authorize(Roles = "Manager,Owner")] + public async Task CreateSection( + string cafeId, + string branchId, + [FromBody] CreateTableSectionRequest request, + ITenantContext tenant, + CancellationToken ct = default) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (!await _tables.CanAccessBranchAsync(cafeId, branchId, tenant.UserId, tenant.Role, ct)) + return Forbid(); + + var validation = await _createSectionValidator.ValidateAsync(request, ct); + if (!validation.IsValid) return BadRequest(ValidationError(validation)); + + var result = await _tables.CreateBranchSectionAsync(cafeId, branchId, request, ct); + return BranchOpResult(result); + } + + [HttpPatch("sections/{sectionId}")] + [Authorize(Roles = "Manager,Owner")] + public async Task PatchSection( + string cafeId, + string branchId, + string sectionId, + [FromBody] PatchTableSectionRequest request, + ITenantContext tenant, + CancellationToken ct = default) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (!await _tables.CanAccessBranchAsync(cafeId, branchId, tenant.UserId, tenant.Role, ct)) + return Forbid(); + + var validation = await _patchSectionValidator.ValidateAsync(request, ct); + if (!validation.IsValid) return BadRequest(ValidationError(validation)); + + var result = await _tables.PatchBranchSectionAsync(cafeId, branchId, sectionId, request, ct); + return BranchOpResult(result); + } + + [HttpDelete("sections/{sectionId}")] + [Authorize(Roles = "Manager,Owner")] + public async Task DeleteSection( + string cafeId, + string branchId, + string sectionId, + ITenantContext tenant, + CancellationToken ct = default) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (!await _tables.CanAccessBranchAsync(cafeId, branchId, tenant.UserId, tenant.Role, ct)) + return Forbid(); + + var result = await _tables.DeleteBranchSectionAsync(cafeId, branchId, sectionId, ct); + return BranchOpResult(result); + } + + private IActionResult BranchOpResult(BranchTableOperationResult result) + { + if (result.Success && result.Data is not null) + return Ok(new ApiResponse(true, result.Data)); + + var code = result.ErrorCode ?? "REQUEST_FAILED"; + var status = code is "TABLE_HAS_OPEN_ORDER" or "TABLE_SECTION_HAS_TABLES" + ? StatusCodes.Status409Conflict + : StatusCodes.Status400BadRequest; + return StatusCode(status, + new ApiResponse(false, null, new ApiError(code, result.Message ?? code))); + } + +} diff --git a/src/Meezi.API/Controllers/BranchesController.cs b/src/Meezi.API/Controllers/BranchesController.cs new file mode 100644 index 0000000..b9588d5 --- /dev/null +++ b/src/Meezi.API/Controllers/BranchesController.cs @@ -0,0 +1,306 @@ +using FluentValidation; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Meezi.API.Models.Branches; +using Meezi.API.Services; +using Meezi.Core.Constants; +using Meezi.Core.Entities; +using Meezi.Core.Enums; +using Meezi.Core.Interfaces; +using Meezi.Core.Utilities; +using Meezi.Infrastructure.Data; +using Meezi.Shared; + +namespace Meezi.API.Controllers; + +[Route("api/cafes/{cafeId}/branches")] +public class BranchesController : CafeApiControllerBase +{ + private const string PlanLimitMessage = + "Branch limit reached for your plan. Upgrade to Pro or Business to add more branches."; + + private readonly AppDbContext _db; + private readonly IBranchLifecycleService _lifecycle; + private readonly IValidator _createValidator; + private readonly IValidator _patchValidator; + + public BranchesController( + AppDbContext db, + IBranchLifecycleService lifecycle, + IValidator createValidator, + IValidator patchValidator) + { + _db = db; + _lifecycle = lifecycle; + _createValidator = createValidator; + _patchValidator = patchValidator; + } + + [HttpGet] + public async Task List( + string cafeId, + ITenantContext tenant, + [FromQuery] bool activeOnly = false, + [FromQuery] bool includePendingDeletion = false, + CancellationToken ct = default) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + + var now = DateTime.UtcNow; + List branches; + + if (includePendingDeletion) + { + branches = await _db.Branches + .IgnoreQueryFilters() + .Where(b => b.CafeId == cafeId + && (b.DeletedAt == null + || (b.ScheduledPermanentDeleteAt != null && b.ScheduledPermanentDeleteAt > now))) + .OrderBy(b => b.DeletedAt == null ? 0 : 1) + .ThenBy(b => b.Name) + .ToListAsync(ct); + } + else + { + var query = _db.Branches.Where(b => b.CafeId == cafeId); + if (activeOnly) + query = query.Where(b => b.IsActive); + branches = await query.OrderBy(b => b.Name).ToListAsync(ct); + } + + var branchIds = branches.Select(b => b.Id).ToList(); + var managers = await _db.Employees + .AsNoTracking() + .IgnoreQueryFilters() + .Where(e => e.CafeId == cafeId && e.BranchId != null && branchIds.Contains(e.BranchId!) + && e.Role == EmployeeRole.Manager && e.DeletedAt == null) + .ToListAsync(ct); + var managerByBranch = managers + .GroupBy(e => e.BranchId!) + .ToDictionary(g => g.Key, g => g.First()); + + var data = branches.Select(b => + { + managerByBranch.TryGetValue(b.Id, out var mgr); + return ToDto(b, mgr?.Phone, mgr?.Name, now); + }).ToList(); + + return Ok(new ApiResponse>(true, data)); + } + + [HttpPost] + public async Task Create( + string cafeId, + [FromBody] CreateBranchRequest request, + ITenantContext tenant, + CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsureOwner(tenant) is { } ownerDenied) return ownerDenied; + + var validation = await _createValidator.ValidateAsync(request, ct); + if (!validation.IsValid) + { + var first = validation.Errors.First(); + return BadRequest(new ApiResponse(false, null, + new ApiError("VALIDATION_ERROR", first.ErrorMessage, first.PropertyName))); + } + + var loginPhone = PhoneNormalizer.Normalize(request.LoginPhone); + var phoneTaken = await _db.Employees.AnyAsync( + e => e.CafeId == cafeId && e.Phone == loginPhone && e.DeletedAt == null, ct); + if (phoneTaken) + { + return BadRequest(new ApiResponse(false, null, + new ApiError("PHONE_ALREADY_REGISTERED", + "This mobile number is already used for login at this cafe."))); + } + + var tier = tenant.PlanTier ?? PlanTier.Free; + var count = await _db.Branches.CountAsync(b => b.CafeId == cafeId, ct); + var max = PlanLimits.MaxBranches(tier); + if (count >= max) + return StatusCode(403, new ApiResponse(false, null, + new ApiError("PLAN_LIMIT_REACHED", PlanLimitMessage))); + + var now = DateTime.UtcNow; + var branchId = $"branch_{Guid.NewGuid():N}"[..24]; + var managerName = string.IsNullOrWhiteSpace(request.ManagerName) + ? request.Name.Trim() + : request.ManagerName.Trim(); + + var branch = new Branch + { + Id = branchId, + CafeId = cafeId, + Name = request.Name.Trim(), + Address = request.Address?.Trim(), + City = request.City?.Trim(), + Phone = request.Phone?.Trim(), + IsActive = true, + CreatedAt = now, + UpdatedAt = now + }; + + var employee = new Employee + { + Id = $"emp_{Guid.NewGuid():N}"[..24], + CafeId = cafeId, + BranchId = branchId, + Name = managerName, + Phone = loginPhone, + Role = EmployeeRole.Manager, + CreatedAt = now + }; + + _db.Branches.Add(branch); + _db.Employees.Add(employee); + await _db.SaveChangesAsync(ct); + + return Ok(new ApiResponse(true, ToDto(branch, loginPhone, managerName, now))); + } + + [HttpPatch("{branchId}")] + public async Task Patch( + string cafeId, + string branchId, + [FromBody] PatchBranchRequest request, + ITenantContext tenant, + CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsureOwner(tenant) is { } ownerDenied) return ownerDenied; + + var validation = await _patchValidator.ValidateAsync(request, ct); + if (!validation.IsValid) + { + var first = validation.Errors.First(); + return BadRequest(new ApiResponse(false, null, + new ApiError("VALIDATION_ERROR", first.ErrorMessage, first.PropertyName))); + } + + var branch = await _db.Branches.FirstOrDefaultAsync(b => b.Id == branchId && b.CafeId == cafeId, ct); + if (branch is null) return NotFoundError(); + + if (request.TaxRate.HasValue) + { + var cafe = await _db.Cafes.AsNoTracking().FirstAsync(c => c.Id == cafeId, ct); + if (!cafe.AllowBranchTaxOverride) + { + return BadRequest(new ApiResponse(false, null, + new ApiError("TAX_OVERRIDE_NOT_ALLOWED", + "تغییر نرخ مالیات شعبه توسط مالک غیرفعال شده است"))); + } + } + + if (request.Name is not null) branch.Name = request.Name.Trim(); + if (request.Address is not null) branch.Address = request.Address.Trim(); + if (request.City is not null) branch.City = request.City.Trim(); + if (request.Phone is not null) branch.Phone = request.Phone.Trim(); + if (request.IsActive.HasValue) branch.IsActive = request.IsActive.Value; + if (request.LogoUrl is not null) branch.LogoUrl = string.IsNullOrWhiteSpace(request.LogoUrl) ? null : request.LogoUrl.Trim(); + if (request.WelcomeText is not null) branch.WelcomeText = string.IsNullOrWhiteSpace(request.WelcomeText) ? null : request.WelcomeText.Trim(); + if (request.AccentColor is not null) branch.AccentColor = string.IsNullOrWhiteSpace(request.AccentColor) ? null : request.AccentColor.Trim(); + if (request.WifiPassword is not null) branch.WifiPassword = string.IsNullOrWhiteSpace(request.WifiPassword) ? null : request.WifiPassword.Trim(); + if (request.TaxRate.HasValue) branch.TaxRate = request.TaxRate.Value; + + branch.UpdatedAt = DateTime.UtcNow; + await _db.SaveChangesAsync(ct); + + var mgr = await _db.Employees.AsNoTracking() + .FirstOrDefaultAsync(e => e.CafeId == cafeId && e.BranchId == branchId + && e.Role == EmployeeRole.Manager && e.DeletedAt == null, ct); + + return Ok(new ApiResponse(true, ToDto(branch, mgr?.Phone, mgr?.Name, DateTime.UtcNow))); + } + + [HttpDelete("{branchId}")] + public async Task Delete( + string cafeId, + string branchId, + ITenantContext tenant, + CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsureOwner(tenant) is { } ownerDenied) return ownerDenied; + + var (ok, code, message) = await _lifecycle.ScheduleDeletionAsync(cafeId, branchId, ct); + if (!ok) + { + return code switch + { + "NOT_FOUND" => NotFoundError(), + "LAST_BRANCH" => BadRequest(new ApiResponse(false, null, + new ApiError(code, message ?? "Cannot delete the last branch."))), + _ => BadRequest(new ApiResponse(false, null, + new ApiError(code ?? "DELETE_FAILED", message ?? "Could not delete branch."))) + }; + } + + var branch = await _db.Branches + .IgnoreQueryFilters() + .FirstAsync(b => b.Id == branchId && b.CafeId == cafeId, ct); + var mgr = await _db.Employees.AsNoTracking() + .IgnoreQueryFilters() + .FirstOrDefaultAsync(e => e.CafeId == cafeId && e.BranchId == branchId + && e.Role == EmployeeRole.Manager && e.DeletedAt == branch.DeletedAt, ct); + + return Ok(new ApiResponse(true, + ToDto(branch, mgr?.Phone, mgr?.Name, DateTime.UtcNow))); + } + + [HttpPost("{branchId}/restore")] + public async Task Restore( + string cafeId, + string branchId, + ITenantContext tenant, + CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsureOwner(tenant) is { } ownerDenied) return ownerDenied; + + var (ok, code, message) = await _lifecycle.RestoreAsync(cafeId, branchId, ct); + if (!ok) + { + return code switch + { + "NOT_FOUND" => NotFoundError(), + "PURGE_EXPIRED" => BadRequest(new ApiResponse(false, null, + new ApiError(code, message ?? "Recovery period has ended."))), + _ => BadRequest(new ApiResponse(false, null, + new ApiError(code ?? "RESTORE_FAILED", message ?? "Could not restore branch."))) + }; + } + + var branch = await _db.Branches.FirstAsync(b => b.Id == branchId && b.CafeId == cafeId, ct); + var mgr = await _db.Employees.AsNoTracking() + .FirstOrDefaultAsync(e => e.CafeId == cafeId && e.BranchId == branchId + && e.Role == EmployeeRole.Manager && e.DeletedAt == null, ct); + + return Ok(new ApiResponse(true, ToDto(branch, mgr?.Phone, mgr?.Name, DateTime.UtcNow))); + } + + private static BranchDto ToDto(Branch b, string? loginPhone, string? managerName, DateTime utcNow) + { + var pending = b.DeletedAt is not null + && b.ScheduledPermanentDeleteAt is not null + && b.ScheduledPermanentDeleteAt > utcNow; + int? daysLeft = pending + ? Math.Max(0, (int)Math.Ceiling((b.ScheduledPermanentDeleteAt!.Value - utcNow).TotalDays)) + : null; + + return new BranchDto( + b.Id, + b.Name, + b.Address, + b.City, + b.Phone, + b.IsActive && !pending, + loginPhone, + managerName, + pending, + b.DeletedAt, + b.ScheduledPermanentDeleteAt, + daysLeft); + } +} diff --git a/src/Meezi.API/Controllers/CafeApiControllerBase.cs b/src/Meezi.API/Controllers/CafeApiControllerBase.cs new file mode 100644 index 0000000..a2cae9a --- /dev/null +++ b/src/Meezi.API/Controllers/CafeApiControllerBase.cs @@ -0,0 +1,38 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Meezi.Core.Enums; +using Meezi.Core.Interfaces; +using Meezi.Shared; + +namespace Meezi.API.Controllers; + +[Authorize] +[ApiController] +public abstract class CafeApiControllerBase : ControllerBase +{ + protected IActionResult? EnsureCafeAccess(string routeCafeId, ITenantContext tenant) + { + if (string.IsNullOrEmpty(tenant.CafeId) || tenant.CafeId != routeCafeId) + return StatusCode(StatusCodes.Status403Forbidden, + new ApiResponse(false, null, new ApiError("FORBIDDEN", "You do not have access to this cafe."))); + return null; + } + + protected IActionResult? EnsureOwner(ITenantContext tenant) + { + if (tenant.Role == EmployeeRole.Owner) + return null; + return StatusCode(StatusCodes.Status403Forbidden, + new ApiResponse(false, null, + new ApiError("OWNER_REQUIRED", "Only the cafe owner can perform this action."))); + } + + protected static ApiResponse ValidationError(FluentValidation.Results.ValidationResult validation) + { + var first = validation.Errors.First(); + return new ApiResponse(false, null, new ApiError("VALIDATION_ERROR", first.ErrorMessage, first.PropertyName)); + } + + protected IActionResult NotFoundError(string message = "Resource not found.") => + NotFound(new ApiResponse(false, null, new ApiError("NOT_FOUND", message))); +} diff --git a/src/Meezi.API/Controllers/CafeDiscoverProfileController.cs b/src/Meezi.API/Controllers/CafeDiscoverProfileController.cs new file mode 100644 index 0000000..c09e358 --- /dev/null +++ b/src/Meezi.API/Controllers/CafeDiscoverProfileController.cs @@ -0,0 +1,70 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Meezi.API.Models.Discover; +using Meezi.API.Services; +using Meezi.Core.Enums; +using Meezi.Core.Interfaces; +using Meezi.Infrastructure.Data; +using Meezi.Infrastructure.Discover; +using Meezi.Infrastructure.Services.Platform; +using Meezi.Shared; +using Microsoft.EntityFrameworkCore; + +namespace Meezi.API.Controllers; + +[Authorize] +[Route("api/cafes/{cafeId}/discover-profile")] +public class CafeDiscoverProfileController : CafeApiControllerBase +{ + private readonly AppDbContext _db; + + public CafeDiscoverProfileController(AppDbContext db) => _db = db; + + [HttpGet] + public async Task Get(string cafeId, ITenantContext tenant, CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) + return denied; + + var cafe = await _db.Cafes.AsNoTracking() + .FirstOrDefaultAsync(c => c.Id == cafeId, cancellationToken: ct); + if (cafe is null) + return NotFound(new ApiResponse(false, null, new ApiError("NOT_FOUND", "Cafe not found."))); + + var profile = CafeDiscoverProfileSerializer.Deserialize(cafe.DiscoverProfileJson); + return Ok(new ApiResponse(true, CafeDiscoverProfileMapping.ToDto(profile))); + } + + [HttpPut] + public async Task Put( + string cafeId, + [FromBody] UpsertCafeDiscoverProfileRequest request, + ITenantContext tenant, + IPlatformCatalogService catalog, + CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) + return denied; + + var planTier = tenant.PlanTier ?? PlanTier.Free; + if (!await catalog.IsFeatureEnabledForCafeAsync(cafeId, planTier, "discover_profile", ct)) + { + return StatusCode( + StatusCodes.Status403Forbidden, + new ApiResponse( + false, + null, + new ApiError("PLAN_FEATURE_DISABLED", "Discover profile is not included in your plan. Upgrade to enable it."))); + } + + var cafe = await _db.Cafes.FirstOrDefaultAsync(c => c.Id == cafeId, cancellationToken: ct); + if (cafe is null) + return NotFound(new ApiResponse(false, null, new ApiError("NOT_FOUND", "Cafe not found."))); + + var profile = CafeDiscoverProfileMapping.FromRequest(request); + cafe.DiscoverProfileJson = CafeDiscoverProfileSerializer.Serialize(profile); + await _db.SaveChangesAsync(ct); + + return Ok(new ApiResponse(true, CafeDiscoverProfileMapping.ToDto(profile))); + } +} diff --git a/src/Meezi.API/Controllers/CafePlatformController.cs b/src/Meezi.API/Controllers/CafePlatformController.cs new file mode 100644 index 0000000..ebcb91d --- /dev/null +++ b/src/Meezi.API/Controllers/CafePlatformController.cs @@ -0,0 +1,39 @@ +using Microsoft.AspNetCore.Mvc; +using Meezi.Infrastructure.Services.Platform; +using Meezi.Core.Interfaces; +using Meezi.Shared; + +namespace Meezi.API.Controllers; + +[Route("api/cafes/{cafeId}/platform")] +public class CafePlatformController : CafeApiControllerBase +{ + private readonly IPlatformCatalogService _catalog; + + public CafePlatformController(IPlatformCatalogService catalog) + { + _catalog = catalog; + } + + [HttpGet("features")] + public async Task GetFeatures(string cafeId, ITenantContext tenant, CancellationToken cancellationToken) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (tenant.PlanTier is null) + return Ok(new ApiResponse(true, new Dictionary())); + + var features = await _catalog.GetEffectiveFeaturesForCafeAsync( + cafeId, + tenant.PlanTier.Value, + cancellationToken); + + return Ok(new ApiResponse(true, features)); + } + + [HttpGet("plans")] + public async Task GetPublicPlans(CancellationToken cancellationToken) + { + var plans = await _catalog.GetPlansAsync(cancellationToken); + return Ok(new ApiResponse(true, plans)); + } +} diff --git a/src/Meezi.API/Controllers/CafePublicProfileController.cs b/src/Meezi.API/Controllers/CafePublicProfileController.cs new file mode 100644 index 0000000..baba028 --- /dev/null +++ b/src/Meezi.API/Controllers/CafePublicProfileController.cs @@ -0,0 +1,219 @@ +using System.Text.Json; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Meezi.API.Models.Public; +using Meezi.API.Services; +using Meezi.Core.Discover; +using Meezi.Core.Interfaces; +using Meezi.Infrastructure.Data; +using Meezi.Shared; +using Microsoft.EntityFrameworkCore; + +namespace Meezi.API.Controllers; + +/// +/// Allows cafe owners to manage their public-facing profile: +/// description, gallery (up to 8 photos), working hours, instagram, website. +/// Route: /api/cafes/{cafeId}/public-profile +/// +[Authorize] +[Route("api/cafes/{cafeId}/public-profile")] +public class CafePublicProfileController : CafeApiControllerBase +{ + private const int MaxGalleryPhotos = 8; + + private readonly AppDbContext _db; + private readonly IMediaStorageService _media; + + private static readonly JsonSerializerOptions _jsonOpts = new() + { + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + + public CafePublicProfileController(AppDbContext db, IMediaStorageService media) + { + _db = db; + _media = media; + } + + // ── GET ────────────────────────────────────────────────────────────────── + + [HttpGet] + public async Task Get(string cafeId, ITenantContext tenant, CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + + var cafe = await _db.Cafes.AsNoTracking() + .FirstOrDefaultAsync(c => c.Id == cafeId, ct); + if (cafe is null) + return NotFound(Fail("NOT_FOUND", "Cafe not found.")); + + var hours = Deserialize(cafe.WorkingHoursJson); + var gallery = Deserialize>(cafe.GalleryJson) ?? []; + + return Ok(new ApiResponse(true, new CafeProfileEditDto( + cafe.Description, + gallery, + cafe.InstagramHandle, + cafe.WebsiteUrl, + ToHoursDto(hours)))); + } + + // ── PUT (description / social / hours) ─────────────────────────────────── + + [HttpPut] + public async Task Put( + string cafeId, + [FromBody] UpdateCafePublicProfileRequest request, + ITenantContext tenant, + CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + + var cafe = await _db.Cafes.FirstOrDefaultAsync(c => c.Id == cafeId, ct); + if (cafe is null) + return NotFound(Fail("NOT_FOUND", "Cafe not found.")); + + // Description + if (request.Description is not null) + cafe.Description = request.Description.Trim(); + + // Instagram handle — strip leading @ if present + if (request.InstagramHandle is not null) + cafe.InstagramHandle = request.InstagramHandle.TrimStart('@').Trim().ToLowerInvariant(); + + // Website URL + if (request.WebsiteUrl is not null) + cafe.WebsiteUrl = request.WebsiteUrl.Trim(); + + // Working hours + if (request.WorkingHours is not null) + cafe.WorkingHoursJson = JsonSerializer.Serialize(ToHoursSchedule(request.WorkingHours), _jsonOpts); + + await _db.SaveChangesAsync(ct); + + var gallery = Deserialize>(cafe.GalleryJson) ?? []; + var hours = Deserialize(cafe.WorkingHoursJson); + + return Ok(new ApiResponse(true, new CafeProfileEditDto( + cafe.Description, + gallery, + cafe.InstagramHandle, + cafe.WebsiteUrl, + ToHoursDto(hours)))); + } + + // ── POST gallery/upload ─────────────────────────────────────────────────── + + [HttpPost("gallery")] + [RequestSizeLimit(8 * 1024 * 1024)] + public async Task UploadGalleryPhoto( + string cafeId, + [FromForm] IFormFile photo, + ITenantContext tenant, + CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + + if (photo is null || photo.Length == 0) + return BadRequest(Fail("NO_FILE", "No photo provided.")); + + var cafe = await _db.Cafes.FirstOrDefaultAsync(c => c.Id == cafeId, ct); + if (cafe is null) + return NotFound(Fail("NOT_FOUND", "Cafe not found.")); + + var gallery = Deserialize>(cafe.GalleryJson) ?? []; + + if (gallery.Count >= MaxGalleryPhotos) + return BadRequest(Fail("GALLERY_FULL", $"Maximum {MaxGalleryPhotos} gallery photos allowed. Remove one first.")); + + var url = await _media.SaveCafeGalleryPhotoAsync(cafeId, photo, ct); + if (url is null) + return StatusCode(422, Fail("UPLOAD_FAILED", "Could not process image. Check format and size (max 5 MB, JPEG/PNG/WebP).")); + + gallery.Add(url); + cafe.GalleryJson = JsonSerializer.Serialize(gallery, _jsonOpts); + await _db.SaveChangesAsync(ct); + + return Ok(new ApiResponse(true, new GalleryDto(gallery))); + } + + // ── DELETE gallery photo ────────────────────────────────────────────────── + + [HttpDelete("gallery")] + public async Task RemoveGalleryPhoto( + string cafeId, + [FromQuery] string url, + ITenantContext tenant, + CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + + if (string.IsNullOrWhiteSpace(url)) + return BadRequest(Fail("NO_URL", "Provide ?url= of the photo to remove.")); + + var cafe = await _db.Cafes.FirstOrDefaultAsync(c => c.Id == cafeId, ct); + if (cafe is null) + return NotFound(Fail("NOT_FOUND", "Cafe not found.")); + + var gallery = Deserialize>(cafe.GalleryJson) ?? []; + var removed = gallery.Remove(url); + + if (!removed) + return NotFound(Fail("NOT_IN_GALLERY", "URL not found in gallery.")); + + cafe.GalleryJson = JsonSerializer.Serialize(gallery, _jsonOpts); + await _db.SaveChangesAsync(ct); + + return Ok(new ApiResponse(true, new GalleryDto(gallery))); + } + + // ── helpers ─────────────────────────────────────────────────────────────── + + private static ApiResponse Fail(string code, string message) => + new(false, null, new ApiError(code, message)); + + private static T? Deserialize(string? json) where T : class + { + if (string.IsNullOrWhiteSpace(json)) return null; + try { return JsonSerializer.Deserialize(json, _jsonOpts); } + catch { return null; } + } + + private static WorkingHoursPublicDto? ToHoursDto(WorkingHoursSchedule? h) + { + if (h is null) return null; + DaySchedulePublicDto? M(DaySchedule? d) => + d is null ? null : new DaySchedulePublicDto(d.IsOpen, d.Open, d.Close); + return new WorkingHoursPublicDto(M(h.Sat), M(h.Sun), M(h.Mon), M(h.Tue), M(h.Wed), M(h.Thu), M(h.Fri)); + } + + private static WorkingHoursSchedule ToHoursSchedule(WorkingHoursPublicDto dto) + { + static DaySchedule? M(DaySchedulePublicDto? d) => + d is null ? null : new DaySchedule { IsOpen = d.IsOpen, Open = d.Open, Close = d.Close }; + return new WorkingHoursSchedule + { + Sat = M(dto.Sat), Sun = M(dto.Sun), Mon = M(dto.Mon), + Tue = M(dto.Tue), Wed = M(dto.Wed), Thu = M(dto.Thu), Fri = M(dto.Fri) + }; + } +} + +// ── Request / response DTOs ─────────────────────────────────────────────────── + +public record UpdateCafePublicProfileRequest( + string? Description, + string? InstagramHandle, + string? WebsiteUrl, + WorkingHoursPublicDto? WorkingHours); + +public record CafeProfileEditDto( + string? Description, + IReadOnlyList GalleryUrls, + string? InstagramHandle, + string? WebsiteUrl, + WorkingHoursPublicDto? WorkingHours); + +public record GalleryDto(IReadOnlyList GalleryUrls); diff --git a/src/Meezi.API/Controllers/CafeReviewsController.cs b/src/Meezi.API/Controllers/CafeReviewsController.cs new file mode 100644 index 0000000..81ce30a --- /dev/null +++ b/src/Meezi.API/Controllers/CafeReviewsController.cs @@ -0,0 +1,69 @@ +using FluentValidation; +using Microsoft.AspNetCore.Mvc; +using Meezi.API.Models.Public; +using Meezi.API.Services; +using Meezi.Core.Interfaces; +using Meezi.Shared; + +namespace Meezi.API.Controllers; + +[Route("api/cafes/{cafeId}/reviews")] +public class CafeReviewsController : CafeApiControllerBase +{ + private readonly IReviewService _reviews; + private readonly IValidator _replyValidator; + + public CafeReviewsController(IReviewService reviews, IValidator replyValidator) + { + _reviews = reviews; + _replyValidator = replyValidator; + } + + [HttpGet] + public async Task List( + string cafeId, + ITenantContext tenant, + CancellationToken ct, + [FromQuery] int page = 1, + [FromQuery] int pageSize = 20) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var data = await _reviews.GetReviewsAsync(cafeId, page, pageSize, publicOnly: false, ct); + return Ok(new ApiResponse>(true, data)); + } + + [HttpPatch("{reviewId}/reply")] + public async Task Reply( + string cafeId, + string reviewId, + [FromBody] ReplyCafeReviewRequest request, + ITenantContext tenant, + CancellationToken ct = default) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var validation = await _replyValidator.ValidateAsync(request, ct); + if (!validation.IsValid) + { + var first = validation.Errors.First(); + return BadRequest(new ApiResponse(false, null, new ApiError("VALIDATION_ERROR", first.ErrorMessage, first.PropertyName))); + } + + var data = await _reviews.ReplyReviewAsync(cafeId, reviewId, request.Reply, ct); + if (data is null) return NotFoundError(); + return Ok(new ApiResponse(true, data)); + } + + [HttpPatch("{reviewId}/visibility")] + public async Task SetVisibility( + string cafeId, + string reviewId, + [FromBody] HideCafeReviewRequest request, + ITenantContext tenant, + CancellationToken ct = default) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var data = await _reviews.SetHiddenAsync(cafeId, reviewId, request.IsHidden, ct); + if (data is null) return NotFoundError(); + return Ok(new ApiResponse(true, data)); + } +} diff --git a/src/Meezi.API/Controllers/CafeSettingsController.cs b/src/Meezi.API/Controllers/CafeSettingsController.cs new file mode 100644 index 0000000..a0d6475 --- /dev/null +++ b/src/Meezi.API/Controllers/CafeSettingsController.cs @@ -0,0 +1,94 @@ +using FluentValidation; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Meezi.API.Models.Cafes; +using Meezi.API.Services; +using Meezi.Core.Interfaces; +using Meezi.Infrastructure.Branding; +using Meezi.Infrastructure.Data; +using Meezi.Shared; + +namespace Meezi.API.Controllers; + +[Route("api/cafes/{cafeId}/settings")] +public class CafeSettingsController : CafeApiControllerBase +{ + private readonly AppDbContext _db; + private readonly IValidator _validator; + + public CafeSettingsController(AppDbContext db, IValidator validator) + { + _db = db; + _validator = validator; + } + + [HttpGet] + public async Task Get(string cafeId, ITenantContext tenant, CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var cafe = await _db.Cafes.AsNoTracking().FirstOrDefaultAsync(c => c.Id == cafeId, ct); + if (cafe is null) return NotFoundError(); + return Ok(new ApiResponse(true, ToDto(cafe))); + } + + [HttpPatch] + public async Task Patch( + string cafeId, + [FromBody] PatchCafeSettingsRequest request, + ITenantContext tenant, + CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + + var validation = await _validator.ValidateAsync(request, ct); + if (!validation.IsValid) + { + var first = validation.Errors.First(); + return BadRequest(new ApiResponse(false, null, + new ApiError("VALIDATION_ERROR", first.ErrorMessage, first.PropertyName))); + } + + if (request.DefaultTaxRate is not null || request.AllowBranchTaxOverride is not null) + { + if (EnsureOwner(tenant) is { } ownerDenied) return ownerDenied; + } + + var cafe = await _db.Cafes.FirstOrDefaultAsync(c => c.Id == cafeId, ct); + if (cafe is null) return NotFoundError(); + + if (request.Name is not null) cafe.Name = request.Name.Trim(); + if (request.Phone is not null) cafe.Phone = request.Phone.Trim(); + if (request.Address is not null) cafe.Address = request.Address.Trim(); + if (request.City is not null) cafe.City = request.City.Trim(); + if (request.Description is not null) cafe.Description = request.Description.Trim(); + if (request.LogoUrl is not null) cafe.LogoUrl = request.LogoUrl.Trim(); + if (request.CoverImageUrl is not null) cafe.CoverImageUrl = request.CoverImageUrl.Trim(); + if (request.SnappfoodVendorId is not null) cafe.SnappfoodVendorId = request.SnappfoodVendorId.Trim(); + if (request.Theme is not null) + cafe.ThemeJson = CafeThemeSerializer.Serialize(CafeThemeMapping.FromDto(request.Theme)); + if (request.DefaultTaxRate is decimal taxRate) + cafe.DefaultTaxRate = taxRate; + if (request.AllowBranchTaxOverride is bool allowTax) + cafe.AllowBranchTaxOverride = allowTax; + + await _db.SaveChangesAsync(ct); + return Ok(new ApiResponse(true, ToDto(cafe))); + } + + private static CafeSettingsDto ToDto(Core.Entities.Cafe cafe) => new( + cafe.Id, + cafe.Name, + cafe.Slug, + cafe.Phone, + cafe.Address, + cafe.City, + cafe.Description, + cafe.LogoUrl, + cafe.CoverImageUrl, + cafe.SnappfoodVendorId, + cafe.PlanTier.ToString(), + cafe.PlanExpiresAt, + CafeThemeMapping.FromJson(cafe.ThemeJson), + cafe.DefaultTaxRate, + cafe.AllowBranchTaxOverride); +} diff --git a/src/Meezi.API/Controllers/ConsumerAuthController.cs b/src/Meezi.API/Controllers/ConsumerAuthController.cs new file mode 100644 index 0000000..5ec0fb6 --- /dev/null +++ b/src/Meezi.API/Controllers/ConsumerAuthController.cs @@ -0,0 +1,82 @@ +using FluentValidation; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.RateLimiting; +using Meezi.API.Models.Auth; +using Meezi.API.Services; +using Meezi.Shared; + +namespace Meezi.API.Controllers; + +[ApiController] +[AllowAnonymous] +[Route("api/auth/customer")] +public class ConsumerAuthController : ControllerBase +{ + private readonly IConsumerAuthService _auth; + private readonly IValidator _sendValidator; + private readonly IValidator _verifyValidator; + + public ConsumerAuthController( + IConsumerAuthService auth, + IValidator sendValidator, + IValidator verifyValidator) + { + _auth = auth; + _sendValidator = sendValidator; + _verifyValidator = verifyValidator; + } + + [HttpPost("send-otp")] + [EnableRateLimiting("auth-otp")] + public async Task SendOtp([FromBody] SendOtpRequest request, CancellationToken ct) + { + var validation = await _sendValidator.ValidateAsync(request, ct); + if (!validation.IsValid) return ValidationBadRequest(validation); + + var (success, data, code, message) = await _auth.SendOtpAsync(request, ct); + if (!success) + return AuthError(code, message); + + return Ok(new ApiResponse(true, data)); + } + + [HttpPost("verify-otp")] + [EnableRateLimiting("auth-otp")] + public async Task VerifyOtp([FromBody] VerifyOtpRequest request, CancellationToken ct) + { + var validation = await _verifyValidator.ValidateAsync(request, ct); + if (!validation.IsValid) return ValidationBadRequest(validation); + + var (success, data, code, message) = await _auth.VerifyOtpAsync(request, ct); + if (!success) + return AuthError(code, message); + + return Ok(new ApiResponse(true, data)); + } + + [HttpPost("refresh")] + public async Task Refresh([FromBody] RefreshTokenRequest request, CancellationToken ct) + { + var (success, data, code, message) = await _auth.RefreshAsync(request, ct); + if (!success) + return AuthError(code, message); + + return Ok(new ApiResponse(true, data)); + } + + private IActionResult ValidationBadRequest(FluentValidation.Results.ValidationResult validation) + { + var first = validation.Errors[0]; + return BadRequest(new ApiResponse(false, null, + new ApiError("VALIDATION_ERROR", first.ErrorMessage, first.PropertyName))); + } + + private IActionResult AuthError(string? code, string? message) => + code switch + { + "RATE_LIMITED" => StatusCode(429, new ApiResponse(false, null, new ApiError(code, message ?? "Rate limited."))), + "NOT_FOUND" => NotFound(new ApiResponse(false, null, new ApiError(code, message ?? "Not found."))), + _ => BadRequest(new ApiResponse(false, null, new ApiError(code ?? "AUTH_FAILED", message ?? "Authentication failed."))) + }; +} diff --git a/src/Meezi.API/Controllers/CouponsController.cs b/src/Meezi.API/Controllers/CouponsController.cs new file mode 100644 index 0000000..2673c3d --- /dev/null +++ b/src/Meezi.API/Controllers/CouponsController.cs @@ -0,0 +1,102 @@ +using FluentValidation; +using Microsoft.AspNetCore.Mvc; +using Meezi.API.Models.Crm; +using Meezi.API.Services; +using Meezi.Core.Interfaces; +using Meezi.Shared; + +namespace Meezi.API.Controllers; + +[Route("api/cafes/{cafeId}/coupons")] +public class CouponsController : CafeApiControllerBase +{ + private readonly ICouponService _couponService; + private readonly IValidator _createValidator; + + public CouponsController( + ICouponService couponService, + IValidator createValidator) + { + _couponService = couponService; + _createValidator = createValidator; + } + + [HttpGet] + public async Task GetAll(string cafeId, ITenantContext tenant, CancellationToken cancellationToken) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var data = await _couponService.GetAllAsync(cafeId, cancellationToken); + return Ok(new ApiResponse>(true, data)); + } + + [HttpGet("{id}")] + public async Task Get(string cafeId, string id, ITenantContext tenant, CancellationToken cancellationToken) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var data = await _couponService.GetAsync(cafeId, id, cancellationToken); + if (data is null) return NotFoundError(); + return Ok(new ApiResponse(true, data)); + } + + [HttpPost("validate")] + public async Task Validate( + string cafeId, + [FromBody] ValidateCouponRequest request, + ITenantContext tenant, + CancellationToken cancellationToken) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + + var (data, error) = await _couponService.ValidateAsync(cafeId, request, cancellationToken); + if (error is not null) + return BadRequest(new ApiResponse(false, null, error)); + + return Ok(new ApiResponse(true, data)); + } + + [HttpPost] + public async Task Create( + string cafeId, + [FromBody] CreateCouponRequest request, + ITenantContext tenant, + CancellationToken cancellationToken) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var validation = await _createValidator.ValidateAsync(request, cancellationToken); + if (!validation.IsValid) return BadRequest(ValidationError(validation)); + + var data = await _couponService.CreateAsync(cafeId, request, cancellationToken); + if (data is null) + return BadRequest(new ApiResponse(false, null, + new ApiError("DUPLICATE_CODE", "Coupon code already exists."))); + + return Ok(new ApiResponse(true, data)); + } + + [HttpPatch("{id}")] + public async Task Update( + string cafeId, + string id, + [FromBody] UpdateCouponRequest request, + ITenantContext tenant, + CancellationToken cancellationToken) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var data = await _couponService.UpdateAsync(cafeId, id, request, cancellationToken); + if (data is null) return NotFoundError(); + return Ok(new ApiResponse(true, data)); + } + + [HttpDelete("{id}")] + public async Task Delete( + string cafeId, + string id, + ITenantContext tenant, + CancellationToken cancellationToken) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var deleted = await _couponService.DeleteAsync(cafeId, id, cancellationToken); + if (!deleted) return NotFoundError(); + return Ok(new ApiResponse(true, new { id })); + } +} diff --git a/src/Meezi.API/Controllers/CustomerMeController.cs b/src/Meezi.API/Controllers/CustomerMeController.cs new file mode 100644 index 0000000..3bb0a4e --- /dev/null +++ b/src/Meezi.API/Controllers/CustomerMeController.cs @@ -0,0 +1,36 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Meezi.API.Models.Consumer; +using Meezi.API.Services; +using Meezi.Core.Constants; +using Meezi.Shared; + +namespace Meezi.API.Controllers; + +[ApiController] +[Authorize] +[Route("api/customers/me")] +public class CustomerMeController : ControllerBase +{ + private readonly IConsumerOrdersService _orders; + + public CustomerMeController(IConsumerOrdersService orders) => _orders = orders; + + [HttpGet("orders")] + public async Task GetOrders( + [FromQuery] int page = 1, + [FromQuery] int pageSize = 20, + CancellationToken ct = default) + { + if (User.FindFirst(MeeziClaimTypes.Actor)?.Value != MeeziActorKinds.Consumer) + return Forbid(); + + var phone = User.FindFirst(MeeziClaimTypes.Phone)?.Value; + if (string.IsNullOrEmpty(phone)) + return Unauthorized(new ApiResponse(false, null, new ApiError("UNAUTHORIZED", "Phone claim missing."))); + + var data = await _orders.GetMyOrdersAsync(phone, page, pageSize, ct); + return Ok(new ApiResponse>(true, data)); + } +} diff --git a/src/Meezi.API/Controllers/CustomersController.cs b/src/Meezi.API/Controllers/CustomersController.cs new file mode 100644 index 0000000..10b64fa --- /dev/null +++ b/src/Meezi.API/Controllers/CustomersController.cs @@ -0,0 +1,106 @@ +using FluentValidation; +using Microsoft.AspNetCore.Mvc; +using Meezi.API.Models.Crm; +using Meezi.API.Services; +using Meezi.Core.Interfaces; +using Meezi.Shared; + +namespace Meezi.API.Controllers; + +[Route("api/cafes/{cafeId}/customers")] +public class CustomersController : CafeApiControllerBase +{ + private readonly ICustomerService _customerService; + private readonly IValidator _createValidator; + private readonly IValidator _updateValidator; + + public CustomersController( + ICustomerService customerService, + IValidator createValidator, + IValidator updateValidator) + { + _customerService = customerService; + _createValidator = createValidator; + _updateValidator = updateValidator; + } + + [HttpGet] + public async Task Search( + string cafeId, + [FromQuery] string? q, + ITenantContext tenant, + CancellationToken cancellationToken) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var data = await _customerService.SearchAsync(cafeId, q, cancellationToken); + return Ok(new ApiResponse>(true, data)); + } + + [HttpGet("{id}")] + public async Task Get( + string cafeId, + string id, + ITenantContext tenant, + CancellationToken cancellationToken) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var data = await _customerService.GetAsync(cafeId, id, cancellationToken); + if (data is null) return NotFoundError(); + return Ok(new ApiResponse(true, data)); + } + + [HttpPost] + public async Task Create( + string cafeId, + [FromBody] CreateCustomerRequest request, + ITenantContext tenant, + CancellationToken cancellationToken) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var validation = await _createValidator.ValidateAsync(request, cancellationToken); + if (!validation.IsValid) return BadRequest(ValidationError(validation)); + + var data = await _customerService.CreateAsync(cafeId, request, cancellationToken); + if (data is null) + return BadRequest(new ApiResponse(false, null, + new ApiError("DUPLICATE_PHONE", "A customer with this phone already exists."))); + + return Ok(new ApiResponse(true, data)); + } + + [HttpPatch("{id}")] + public async Task Update( + string cafeId, + string id, + [FromBody] UpdateCustomerRequest request, + ITenantContext tenant, + CancellationToken cancellationToken) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var validation = await _updateValidator.ValidateAsync(request, cancellationToken); + if (!validation.IsValid) return BadRequest(ValidationError(validation)); + + var existing = await _customerService.GetAsync(cafeId, id, cancellationToken); + if (existing is null) return NotFoundError(); + + var data = await _customerService.UpdateAsync(cafeId, id, request, cancellationToken); + if (data is null) + return BadRequest(new ApiResponse(false, null, + new ApiError("DUPLICATE_PHONE", "A customer with this phone already exists."))); + + return Ok(new ApiResponse(true, data)); + } + + [HttpDelete("{id}")] + public async Task Delete( + string cafeId, + string id, + ITenantContext tenant, + CancellationToken cancellationToken) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var deleted = await _customerService.DeleteAsync(cafeId, id, cancellationToken); + if (!deleted) return NotFoundError(); + return Ok(new ApiResponse(true, new { id })); + } +} diff --git a/src/Meezi.API/Controllers/DeliveryReportsController.cs b/src/Meezi.API/Controllers/DeliveryReportsController.cs new file mode 100644 index 0000000..f858b21 --- /dev/null +++ b/src/Meezi.API/Controllers/DeliveryReportsController.cs @@ -0,0 +1,31 @@ +using Microsoft.AspNetCore.Mvc; +using Meezi.API.Services.Delivery; +using Meezi.Core.Interfaces; +using Meezi.Shared; + +namespace Meezi.API.Controllers; + +[Route("api/cafes/{cafeId}/reports/delivery")] +public class DeliveryReportsController : CafeApiControllerBase +{ + private readonly IDeliveryFinanceReportService _reports; + + public DeliveryReportsController(IDeliveryFinanceReportService reports) => _reports = reports; + + [HttpGet("revenue")] + public async Task RevenueByPlatform( + string cafeId, + ITenantContext tenant, + [FromQuery] DateTime? from, + [FromQuery] DateTime? to, + CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + + var utcTo = to ?? DateTime.UtcNow; + var utcFrom = from ?? utcTo.AddDays(-30); + + var data = await _reports.GetRevenueByPlatformAsync(cafeId, utcFrom, utcTo, ct); + return Ok(new ApiResponse(true, data)); + } +} diff --git a/src/Meezi.API/Controllers/DigikalaWebhookController.cs b/src/Meezi.API/Controllers/DigikalaWebhookController.cs new file mode 100644 index 0000000..224183a --- /dev/null +++ b/src/Meezi.API/Controllers/DigikalaWebhookController.cs @@ -0,0 +1,51 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Meezi.API.Configuration; +using Meezi.API.Services.Delivery; +using Meezi.Core.Enums; +using Meezi.Shared; +using Microsoft.Extensions.Options; + +namespace Meezi.API.Controllers; + +[ApiController] +[AllowAnonymous] +[Route("api/webhooks/digikala")] +public class DigikalaWebhookController : ControllerBase +{ + private readonly IDeliveryWebhookIngressService _ingress; + private readonly DeliveryPlatformsOptions _options; + + public DigikalaWebhookController( + IDeliveryWebhookIngressService ingress, + IOptions options) + { + _ingress = ingress; + _options = options.Value; + } + + [HttpPost] + public async Task Receive(CancellationToken ct) + { + if (!_options.Digikala.Enabled) + return NotFound(new ApiResponse(false, null, new ApiError("NOT_FOUND", "Digikala webhook disabled."))); + + using var reader = new StreamReader(Request.Body); + var rawBody = await reader.ReadToEndAsync(ct); + + var signature = Request.Headers["X-Digikala-Signature"].FirstOrDefault() + ?? Request.Headers["X-Hub-Signature-256"].FirstOrDefault(); + + var result = await _ingress.ReceiveAsync(DeliveryPlatform.Digikala, rawBody, signature, ct); + if (!result.Accepted) + { + var status = result.ErrorCode == "UNAUTHORIZED" + ? StatusCodes.Status401Unauthorized + : StatusCodes.Status400BadRequest; + return StatusCode(status, new ApiResponse(false, null, + new ApiError(result.ErrorCode ?? "ERROR", result.Message ?? "Failed."))); + } + + return Ok(new ApiResponse(true, new { received = true, logId = result.WebhookLogId })); + } +} diff --git a/src/Meezi.API/Controllers/ExpensesController.cs b/src/Meezi.API/Controllers/ExpensesController.cs new file mode 100644 index 0000000..fe90430 --- /dev/null +++ b/src/Meezi.API/Controllers/ExpensesController.cs @@ -0,0 +1,132 @@ +using FluentValidation; +using Microsoft.AspNetCore.Mvc; +using Meezi.API.Models.Expenses; +using Meezi.API.Services; +using Meezi.Core.Enums; +using Meezi.Core.Interfaces; +using Meezi.Shared; + +namespace Meezi.API.Controllers; + +[Route("api/cafes/{cafeId}/expenses")] +public class ExpensesController : CafeApiControllerBase +{ + private readonly IExpenseService _expenses; + private readonly IValidator _createValidator; + + public ExpensesController( + IExpenseService expenses, + IValidator createValidator) + { + _expenses = expenses; + _createValidator = createValidator; + } + + [HttpPost] + public async Task Create( + string cafeId, + [FromBody] CreateExpenseRequest request, + ITenantContext tenant, + CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (string.IsNullOrEmpty(tenant.UserId)) + return StatusCode(StatusCodes.Status401Unauthorized, + new ApiResponse(false, null, new ApiError("UNAUTHORIZED", "User context is missing."))); + + if (!CanLogExpense(tenant.Role)) + return StatusCode(StatusCodes.Status403Forbidden, + new ApiResponse(false, null, new ApiError("FORBIDDEN", "You cannot log expenses."))); + + var validation = await _createValidator.ValidateAsync(request, ct); + if (!validation.IsValid) return BadRequest(ValidationError(validation)); + + var result = await _expenses.CreateExpenseAsync(cafeId, request, tenant.UserId, ct); + return ExpenseResult(result, StatusCodes.Status201Created); + } + + [HttpGet] + public async Task List( + string cafeId, + [FromQuery] string branchId, + [FromQuery] string from, + [FromQuery] string to, + ITenantContext tenant, + [FromQuery] int page = 1, + [FromQuery] int pageSize = 20, + CancellationToken ct = default) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + + if (string.IsNullOrWhiteSpace(branchId)) + return BadRequest(new ApiResponse(false, null, + new ApiError("VALIDATION_ERROR", "branchId is required.", "branchId"))); + + if (!DateOnly.TryParse(from, out var fromDate) || !DateOnly.TryParse(to, out var toDate)) + return BadRequest(new ApiResponse(false, null, + new ApiError("VALIDATION_ERROR", "Invalid from/to. Use yyyy-MM-dd.", "from"))); + + if (fromDate > toDate) + return BadRequest(new ApiResponse(false, null, + new ApiError("VALIDATION_ERROR", "from must be on or before to.", "from"))); + + var data = await _expenses.GetExpensesAsync(cafeId, branchId, fromDate, toDate, page, pageSize, ct); + return Ok(new PagedApiResponse( + true, + data.Items, + new PagedMeta(data.Total, page, pageSize))); + } + + [HttpDelete("{id}")] + public async Task Delete( + string cafeId, + string id, + ITenantContext tenant, + CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + + if (!CanDeleteExpense(tenant.Role)) + return StatusCode(StatusCodes.Status403Forbidden, + new ApiResponse(false, null, new ApiError("FORBIDDEN", "Only managers can delete expenses."))); + + var result = await _expenses.DeleteExpenseAsync(cafeId, id, ct); + if (!result.Success) + { + return result.ErrorCode switch + { + "NOT_FOUND" => NotFoundError("Expense not found."), + _ => BadRequest(new ApiResponse(false, null, + new ApiError(result.ErrorCode ?? "ERROR", "Delete failed."))) + }; + } + + return Ok(new ApiResponse(true, null)); + } + + private static bool CanLogExpense(EmployeeRole? role) => + role is EmployeeRole.Owner or EmployeeRole.Manager or EmployeeRole.Cashier; + + private static bool CanDeleteExpense(EmployeeRole? role) => + role is EmployeeRole.Owner or EmployeeRole.Manager; + + private IActionResult ExpenseResult(ExpenseServiceResult result, int successStatus = StatusCodes.Status200OK) + { + if (result.Success) + return StatusCode(successStatus, new ApiResponse(true, result.Data)); + + return result.ErrorCode switch + { + "BRANCH_NOT_FOUND" => NotFound(new ApiResponse(false, null, + new ApiError(result.ErrorCode, "Branch not found.", result.Field))), + "SHIFT_NOT_FOUND" => NotFound(new ApiResponse(false, null, + new ApiError(result.ErrorCode, "Shift not found.", result.Field))), + "SHIFT_BRANCH_MISMATCH" => BadRequest(new ApiResponse(false, null, + new ApiError(result.ErrorCode, "Shift does not belong to this branch.", result.Field))), + "SHIFT_ALREADY_CLOSED" => BadRequest(new ApiResponse(false, null, + new ApiError(result.ErrorCode, "Shift is already closed.", result.Field))), + _ => BadRequest(new ApiResponse(false, null, + new ApiError(result.ErrorCode ?? "ERROR", "Could not create expense.", result.Field))) + }; + } +} diff --git a/src/Meezi.API/Controllers/HrController.cs b/src/Meezi.API/Controllers/HrController.cs new file mode 100644 index 0000000..bdf27f1 --- /dev/null +++ b/src/Meezi.API/Controllers/HrController.cs @@ -0,0 +1,221 @@ +using FluentValidation; +using Microsoft.AspNetCore.Mvc; +using Meezi.API.Models.Hr; +using Meezi.API.Services; +using Meezi.Core.Enums; +using Meezi.Core.Interfaces; +using Meezi.Shared; + +namespace Meezi.API.Controllers; + +[Route("api/cafes/{cafeId}")] +public class HrController : CafeApiControllerBase +{ + private readonly IHrService _hr; + private readonly IValidator _leaveValidator; + private readonly IValidator _reviewValidator; + private readonly IValidator _salaryValidator; + + public HrController( + IHrService hr, + IValidator leaveValidator, + IValidator reviewValidator, + IValidator salaryValidator) + { + _hr = hr; + _leaveValidator = leaveValidator; + _reviewValidator = reviewValidator; + _salaryValidator = salaryValidator; + } + + [HttpGet("employees")] + public async Task GetEmployees( + string cafeId, + ITenantContext tenant, + [FromQuery] string? branchId = null, + CancellationToken ct = default) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var data = await _hr.GetEmployeesAsync(cafeId, branchId, ct); + return Ok(new ApiResponse>(true, data)); + } + + [HttpGet("employees/{employeeId}")] + public async Task GetEmployee(string cafeId, string employeeId, ITenantContext tenant, CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var data = await _hr.GetEmployeeAsync(cafeId, employeeId, ct); + if (data is null) return NotFoundError(); + return Ok(new ApiResponse(true, data)); + } + + [HttpGet("employees/{employeeId}/shift/today")] + public async Task GetTodayShift(string cafeId, string employeeId, ITenantContext tenant, CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsureSelfOrManager(employeeId, tenant) is { } forbidden) return forbidden; + var data = await _hr.GetTodayShiftAsync(cafeId, employeeId, ct); + if (data is null) return NotFoundError(); + return Ok(new ApiResponse(true, data)); + } + + [HttpPost("employees/{employeeId}/attendance/clock-in")] + public async Task ClockIn(string cafeId, string employeeId, ITenantContext tenant, CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsureSelfOrManager(employeeId, tenant) is { } forbidden) return forbidden; + var data = await _hr.ClockInAsync(cafeId, employeeId, ct); + if (data is null) return NotFoundError(); + return Ok(new ApiResponse(true, data)); + } + + [HttpPost("employees/{employeeId}/attendance/clock-out")] + public async Task ClockOut(string cafeId, string employeeId, ITenantContext tenant, CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsureSelfOrManager(employeeId, tenant) is { } forbidden) return forbidden; + var data = await _hr.ClockOutAsync(cafeId, employeeId, ct); + if (data is null) return BadRequest(new ApiResponse(false, null, new ApiError("INVALID", "Clock-in required before clock-out."))); + return Ok(new ApiResponse(true, data)); + } + + [HttpGet("attendance")] + public async Task GetAttendance( + string cafeId, + [FromQuery] string? employeeId, + [FromQuery] DateOnly? from, + [FromQuery] DateOnly? to, + ITenantContext tenant, + CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var data = await _hr.GetAttendanceAsync(cafeId, employeeId, from, to, ct); + return Ok(new ApiResponse>(true, data)); + } + + [HttpGet("employees/{employeeId}/shifts")] + public async Task GetShifts(string cafeId, string employeeId, ITenantContext tenant, CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var data = await _hr.GetShiftsAsync(cafeId, employeeId, ct); + return Ok(new ApiResponse>(true, data)); + } + + [HttpPut("employees/{employeeId}/shifts")] + public async Task UpsertShifts( + string cafeId, + string employeeId, + [FromBody] UpsertShiftsRequest request, + ITenantContext tenant, + CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsureManager(tenant) is { } forbidden) return forbidden; + var data = await _hr.UpsertShiftsAsync(cafeId, employeeId, request, ct); + return Ok(new ApiResponse>(true, data)); + } + + [HttpGet("leave-requests")] + public async Task GetLeaveRequests( + string cafeId, + [FromQuery] LeaveStatus? status, + ITenantContext tenant, + CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var data = await _hr.GetLeaveRequestsAsync(cafeId, status, ct); + return Ok(new ApiResponse>(true, data)); + } + + [HttpPost("employees/{employeeId}/leave-requests")] + public async Task CreateLeave( + string cafeId, + string employeeId, + [FromBody] CreateLeaveRequest request, + ITenantContext tenant, + CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsureSelfOrManager(employeeId, tenant) is { } forbidden) return forbidden; + var validation = await _leaveValidator.ValidateAsync(request, ct); + if (!validation.IsValid) return BadRequest(ValidationError(validation)); + + var data = await _hr.CreateLeaveRequestAsync(cafeId, employeeId, request, ct); + if (data is null) return NotFoundError(); + return Ok(new ApiResponse(true, data)); + } + + [HttpPatch("leave-requests/{leaveId}/status")] + public async Task ReviewLeave( + string cafeId, + string leaveId, + [FromBody] ReviewLeaveRequest request, + ITenantContext tenant, + CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsureManager(tenant) is { } forbidden) return forbidden; + var validation = await _reviewValidator.ValidateAsync(request, ct); + if (!validation.IsValid) return BadRequest(ValidationError(validation)); + + var data = await _hr.ReviewLeaveRequestAsync(cafeId, leaveId, tenant.UserId!, request, ct); + if (data is null) return NotFoundError(); + return Ok(new ApiResponse(true, data)); + } + + [HttpGet("salaries")] + public async Task GetSalaries( + string cafeId, + [FromQuery] string? monthYear, + ITenantContext tenant, + CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var data = await _hr.GetSalariesAsync(cafeId, monthYear, ct); + return Ok(new ApiResponse>(true, data)); + } + + [HttpPost("salaries")] + public async Task CreateSalary( + string cafeId, + [FromBody] CreateSalaryRequest request, + ITenantContext tenant, + CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsureManager(tenant) is { } forbidden) return forbidden; + var validation = await _salaryValidator.ValidateAsync(request, ct); + if (!validation.IsValid) return BadRequest(ValidationError(validation)); + + var data = await _hr.CreateSalaryAsync(cafeId, request, ct); + if (data is null) return NotFoundError(); + return Ok(new ApiResponse(true, data)); + } + + [HttpPatch("salaries/{salaryId}/paid")] + public async Task MarkPaid(string cafeId, string salaryId, ITenantContext tenant, CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsureManager(tenant) is { } forbidden) return forbidden; + var data = await _hr.MarkSalaryPaidAsync(cafeId, salaryId, ct); + if (data is null) return NotFoundError(); + return Ok(new ApiResponse(true, data)); + } + + private static IActionResult? EnsureSelfOrManager(string employeeId, ITenantContext tenant) + { + if (tenant.UserId == employeeId) return null; + return EnsureManager(tenant); + } + + private static IActionResult? EnsureManager(ITenantContext tenant) + { + if (tenant.Role is EmployeeRole.Owner or EmployeeRole.Manager) + return null; + + return new ObjectResult(new ApiResponse(false, null, new ApiError("FORBIDDEN", "Manager access required."))) + { + StatusCode = StatusCodes.Status403Forbidden + }; + } +} diff --git a/src/Meezi.API/Controllers/InventoryController.cs b/src/Meezi.API/Controllers/InventoryController.cs new file mode 100644 index 0000000..69dc5b4 --- /dev/null +++ b/src/Meezi.API/Controllers/InventoryController.cs @@ -0,0 +1,140 @@ +using Microsoft.AspNetCore.Mvc; +using Meezi.API.Services; +using Meezi.Core.Interfaces; +using Meezi.Shared; + +namespace Meezi.API.Controllers; + +[Route("api/cafes/{cafeId}/inventory")] +public class InventoryController : CafeApiControllerBase +{ + private readonly IInventoryService _inventory; + + public InventoryController(IInventoryService inventory) => _inventory = inventory; + + [HttpGet("ingredients")] + public async Task List(string cafeId, ITenantContext tenant, CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var data = await _inventory.ListAsync(cafeId, ct); + return Ok(new ApiResponse(true, data)); + } + + [HttpGet("low-stock")] + public async Task LowStock(string cafeId, ITenantContext tenant, CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var data = await _inventory.LowStockAsync(cafeId, ct); + return Ok(new ApiResponse(true, data)); + } + + [HttpPost("ingredients")] + public async Task Create( + string cafeId, + [FromBody] CreateIngredientRequest request, + ITenantContext tenant, + CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (string.IsNullOrWhiteSpace(request.Name)) + return BadRequest(new ApiResponse(false, null, new ApiError("VALIDATION_ERROR", "Name is required."))); + + if (request.QuantityOnHand > 0 && request.TotalPaidToman > 0 && string.IsNullOrWhiteSpace(request.BranchId)) + return BadRequest(new ApiResponse(false, null, + new ApiError("BRANCH_ID_REQUIRED", "Branch is required when recording purchase cost."))); + + var created = await _inventory.CreateAsync(cafeId, request, ct); + return Ok(new ApiResponse(true, created)); + } + + [HttpPatch("ingredients/{ingredientId}")] + public async Task Update( + string cafeId, + string ingredientId, + [FromBody] UpdateIngredientRequest request, + ITenantContext tenant, + CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var updated = await _inventory.UpdateAsync(cafeId, ingredientId, request, ct); + if (updated is null) return NotFoundError(); + return Ok(new ApiResponse(true, updated)); + } + + [HttpPost("ingredients/{ingredientId}/adjust")] + public async Task Adjust( + string cafeId, + string ingredientId, + [FromBody] AdjustStockRequest request, + ITenantContext tenant, + CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + try + { + var updated = await _inventory.AdjustAsync(cafeId, ingredientId, request, tenant.UserId, ct); + if (updated is null) return NotFoundError(); + return Ok(new ApiResponse(true, updated)); + } + catch (InvalidOperationException ex) when (ex.Message is "TOTAL_PAID_REQUIRED" or "BRANCH_ID_REQUIRED") + { + return BadRequest(new ApiResponse(false, null, + new ApiError(ex.Message, ex.Message switch + { + "TOTAL_PAID_REQUIRED" => "Enter total paid for stock received.", + _ => "Branch is required for purchase cost." + }))); + } + } + + [HttpGet("purchases")] + public async Task PurchasesSummary( + string cafeId, + [FromQuery] string branchId, + [FromQuery] DateOnly? from, + [FromQuery] DateOnly? to, + ITenantContext tenant, + CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (string.IsNullOrWhiteSpace(branchId)) + return BadRequest(new ApiResponse(false, null, + new ApiError("BRANCH_ID_REQUIRED", "branchId is required."))); + + var today = DateOnly.FromDateTime(DateTime.UtcNow); + var summary = await _inventory.GetPurchasesSummaryAsync( + cafeId, + branchId, + from ?? today.AddDays(-30), + to ?? today, + ct); + return Ok(new ApiResponse(true, summary)); + } + + [HttpGet("menu-items/{menuItemId}/recipe")] + public async Task GetRecipe( + string cafeId, + string menuItemId, + ITenantContext tenant, + CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var recipe = await _inventory.GetRecipeAsync(cafeId, menuItemId, ct); + if (recipe is null) return NotFoundError("Menu item not found."); + return Ok(new ApiResponse(true, recipe)); + } + + [HttpPut("menu-items/{menuItemId}/recipe")] + public async Task SetRecipe( + string cafeId, + string menuItemId, + [FromBody] SetMenuItemRecipeRequest request, + ITenantContext tenant, + CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var recipe = await _inventory.SetRecipeAsync(cafeId, menuItemId, request, ct); + if (recipe is null) return NotFoundError("Menu item not found."); + return Ok(new ApiResponse(true, recipe)); + } +} diff --git a/src/Meezi.API/Controllers/KitchenStationsController.cs b/src/Meezi.API/Controllers/KitchenStationsController.cs new file mode 100644 index 0000000..5339803 --- /dev/null +++ b/src/Meezi.API/Controllers/KitchenStationsController.cs @@ -0,0 +1,78 @@ +using FluentValidation; +using Microsoft.AspNetCore.Mvc; +using Meezi.API.Models.Kitchen; +using Meezi.API.Services; +using Meezi.Core.Interfaces; +using Meezi.Shared; + +namespace Meezi.API.Controllers; + +[Route("api/cafes/{cafeId}/kitchen-stations")] +public class KitchenStationsController : CafeApiControllerBase +{ + private readonly IKitchenStationService _stations; + private readonly IValidator _createValidator; + private readonly IValidator _updateValidator; + + public KitchenStationsController( + IKitchenStationService stations, + IValidator createValidator, + IValidator updateValidator) + { + _stations = stations; + _createValidator = createValidator; + _updateValidator = updateValidator; + } + + [HttpGet] + public async Task List(string cafeId, ITenantContext tenant, CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var data = await _stations.ListAsync(cafeId, ct); + return Ok(new ApiResponse>(true, data)); + } + + [HttpPost] + public async Task Create( + string cafeId, + [FromBody] CreateKitchenStationRequest request, + ITenantContext tenant, + CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var validation = await _createValidator.ValidateAsync(request, ct); + if (!validation.IsValid) return BadRequest(ValidationError(validation)); + + var data = await _stations.CreateAsync(cafeId, request, ct); + if (data is null) + return BadRequest(new ApiResponse(false, null, new ApiError("INVALID_BRANCH", "Branch not found."))); + + return Ok(new ApiResponse(true, data)); + } + + [HttpPatch("{id}")] + public async Task Update( + string cafeId, + string id, + [FromBody] UpdateKitchenStationRequest request, + ITenantContext tenant, + CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var validation = await _updateValidator.ValidateAsync(request, ct); + if (!validation.IsValid) return BadRequest(ValidationError(validation)); + + var data = await _stations.UpdateAsync(cafeId, id, request, ct); + if (data is null) return NotFoundError(); + return Ok(new ApiResponse(true, data)); + } + + [HttpDelete("{id}")] + public async Task Delete(string cafeId, string id, ITenantContext tenant, CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var ok = await _stations.DeleteAsync(cafeId, id, ct); + if (!ok) return NotFoundError(); + return Ok(new ApiResponse(true, new { id })); + } +} diff --git a/src/Meezi.API/Controllers/MediaController.cs b/src/Meezi.API/Controllers/MediaController.cs new file mode 100644 index 0000000..ae3f26d --- /dev/null +++ b/src/Meezi.API/Controllers/MediaController.cs @@ -0,0 +1,105 @@ +using Microsoft.AspNetCore.Mvc; +using Meezi.API.Services; +using Meezi.Core.Enums; +using Meezi.Core.Interfaces; +using Meezi.Infrastructure.Services.Platform; +using Meezi.Shared; + +namespace Meezi.API.Controllers; + +[Route("api/cafes/{cafeId}/media")] +public class MediaController : CafeApiControllerBase +{ + private readonly IMediaStorageService _media; + + public MediaController(IMediaStorageService media) => _media = media; + + [HttpPost("menu-image")] + [RequestSizeLimit(5 * 1024 * 1024)] + public Task UploadMenuImage( + string cafeId, IFormFile file, ITenantContext tenant, CancellationToken cancellationToken) + => Upload(cafeId, file, tenant, _media.SaveMenuImageAsync, "INVALID_FILE", "Use JPEG/PNG/WebP up to 5MB.", cancellationToken); + + [HttpPost("menu-video")] + [RequestSizeLimit(25 * 1024 * 1024)] + public Task UploadMenuVideo( + string cafeId, IFormFile file, ITenantContext tenant, CancellationToken cancellationToken) + => Upload(cafeId, file, tenant, _media.SaveMenuVideoAsync, "INVALID_FILE", "Use MP4/WebM/MOV up to 25MB.", cancellationToken); + + [HttpPost("menu-model3d")] + [RequestSizeLimit(8 * 1024 * 1024)] + public async Task UploadMenuModel3d( + string cafeId, + IFormFile file, + ITenantContext tenant, + IPlatformCatalogService catalog, + CancellationToken cancellationToken) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var planTier = tenant.PlanTier ?? PlanTier.Free; + if (!await catalog.IsFeatureEnabledForCafeAsync(cafeId, planTier, "menu_3d", cancellationToken)) + { + return StatusCode( + StatusCodes.Status403Forbidden, + new ApiResponse( + false, + null, + new ApiError("PLAN_FEATURE_DISABLED", "3D menu is not included in your plan. Upgrade to enable it."))); + } + + return await Upload( + cafeId, + file, + tenant, + _media.SaveMenuModel3dAsync, + "INVALID_FILE", + "Use GLB (.glb) up to 8MB.", + cancellationToken); + } + + [HttpPost("table-image")] + [RequestSizeLimit(5 * 1024 * 1024)] + public Task UploadTableImage( + string cafeId, IFormFile file, ITenantContext tenant, CancellationToken cancellationToken) + => Upload(cafeId, file, tenant, _media.SaveTableImageAsync, "INVALID_FILE", "Use JPEG/PNG/WebP up to 5MB.", cancellationToken); + + [HttpPost("table-video")] + [RequestSizeLimit(25 * 1024 * 1024)] + public Task UploadTableVideo( + string cafeId, IFormFile file, ITenantContext tenant, CancellationToken cancellationToken) + => Upload(cafeId, file, tenant, _media.SaveTableVideoAsync, "INVALID_FILE", "Use MP4/WebM/MOV up to 25MB.", cancellationToken); + + [HttpPost("cafe-logo")] + [RequestSizeLimit(5 * 1024 * 1024)] + public Task UploadCafeLogo( + string cafeId, IFormFile file, ITenantContext tenant, CancellationToken cancellationToken) + => Upload(cafeId, file, tenant, _media.SaveCafeLogoAsync, "INVALID_FILE", "Use JPEG/PNG/WebP up to 5MB.", cancellationToken); + + [HttpPost("cafe-cover")] + [RequestSizeLimit(5 * 1024 * 1024)] + public Task UploadCafeCover( + string cafeId, IFormFile file, ITenantContext tenant, CancellationToken cancellationToken) + => Upload(cafeId, file, tenant, _media.SaveCafeCoverAsync, "INVALID_FILE", "Use JPEG/PNG/WebP up to 5MB.", cancellationToken); + + private async Task Upload( + string cafeId, + IFormFile file, + ITenantContext tenant, + Func> save, + string errorCode, + string errorMessage, + CancellationToken cancellationToken) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (file is null || file.Length == 0) + return BadRequest(new ApiResponse(false, null, new ApiError("INVALID_FILE", "No file uploaded."))); + + var url = await save(cafeId, file, cancellationToken); + if (url is null) + return BadRequest(new ApiResponse(false, null, new ApiError(errorCode, errorMessage))); + + return Ok(new ApiResponse(true, new UploadResultDto(url))); + } +} + +public record UploadResultDto(string Url); diff --git a/src/Meezi.API/Controllers/MenuController.cs b/src/Meezi.API/Controllers/MenuController.cs new file mode 100644 index 0000000..ab86ad3 --- /dev/null +++ b/src/Meezi.API/Controllers/MenuController.cs @@ -0,0 +1,169 @@ +using FluentValidation; +using Microsoft.AspNetCore.Mvc; +using Meezi.API.Models.Menu; +using Meezi.API.Services; +using Meezi.Core.Enums; +using Meezi.Core.Interfaces; +using Meezi.Shared; + +namespace Meezi.API.Controllers; + +[Route("api/cafes/{cafeId}/menu")] +public class MenuController : CafeApiControllerBase +{ + private readonly IMenuService _menuService; + private readonly IMenuAi3dGenerationService _menuAi3d; + private readonly IValidator _createCategoryValidator; + private readonly IValidator _createItemValidator; + + public MenuController( + IMenuService menuService, + IMenuAi3dGenerationService menuAi3d, + IValidator createCategoryValidator, + IValidator createItemValidator) + { + _menuService = menuService; + _menuAi3d = menuAi3d; + _createCategoryValidator = createCategoryValidator; + _createItemValidator = createItemValidator; + } + + [HttpGet("categories")] + public async Task GetCategories(string cafeId, ITenantContext tenant, CancellationToken cancellationToken) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var data = await _menuService.GetCategoriesAsync(cafeId, cancellationToken); + return Ok(new ApiResponse>(true, data)); + } + + [HttpPost("categories")] + public async Task CreateCategory( + string cafeId, + [FromBody] CreateMenuCategoryRequest request, + ITenantContext tenant, + CancellationToken cancellationToken) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var validation = await _createCategoryValidator.ValidateAsync(request, cancellationToken); + if (!validation.IsValid) return BadRequest(ValidationError(validation)); + + var data = await _menuService.CreateCategoryAsync(cafeId, request, cancellationToken); + return Ok(new ApiResponse(true, data)); + } + + [HttpPatch("categories/{id}")] + public async Task UpdateCategory( + string cafeId, + string id, + [FromBody] UpdateMenuCategoryRequest request, + ITenantContext tenant, + CancellationToken cancellationToken) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var data = await _menuService.UpdateCategoryAsync(cafeId, id, request, cancellationToken); + if (data is null) return NotFoundError(); + return Ok(new ApiResponse(true, data)); + } + + [HttpDelete("categories/{id}")] + public async Task DeleteCategory(string cafeId, string id, ITenantContext tenant, CancellationToken cancellationToken) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var deleted = await _menuService.DeleteCategoryAsync(cafeId, id, cancellationToken); + if (!deleted) return NotFoundError(); + return Ok(new ApiResponse(true, new { id })); + } + + [HttpGet("items")] + public async Task GetItems( + string cafeId, + [FromQuery] string? categoryId, + ITenantContext tenant, + CancellationToken cancellationToken) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var data = await _menuService.GetItemsAsync(cafeId, categoryId, cancellationToken); + return Ok(new ApiResponse>(true, data)); + } + + [HttpPost("items")] + public async Task CreateItem( + string cafeId, + [FromBody] CreateMenuItemRequest request, + ITenantContext tenant, + CancellationToken cancellationToken) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var validation = await _createItemValidator.ValidateAsync(request, cancellationToken); + if (!validation.IsValid) return BadRequest(ValidationError(validation)); + + var data = await _menuService.CreateItemAsync(cafeId, request, cancellationToken); + if (data is null) return NotFoundError("Category not found."); + return Ok(new ApiResponse(true, data)); + } + + [HttpPatch("items/{id}")] + public async Task UpdateItem( + string cafeId, + string id, + [FromBody] UpdateMenuItemRequest request, + ITenantContext tenant, + CancellationToken cancellationToken) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var data = await _menuService.UpdateItemAsync(cafeId, id, request, cancellationToken); + if (data is null) return NotFoundError(); + return Ok(new ApiResponse(true, data)); + } + + [HttpPatch("items/{id}/availability")] + public async Task SetAvailability( + string cafeId, + string id, + [FromBody] UpdateMenuItemAvailabilityRequest request, + ITenantContext tenant, + CancellationToken cancellationToken) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var data = await _menuService.SetAvailabilityAsync(cafeId, id, request.IsAvailable, cancellationToken); + if (data is null) return NotFoundError(); + return Ok(new ApiResponse(true, data)); + } + + [HttpGet("ai-3d/usage")] + public async Task GetAi3dUsage(string cafeId, ITenantContext tenant, CancellationToken cancellationToken) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var tier = tenant.PlanTier ?? PlanTier.Free; + var data = await _menuAi3d.GetUsageAsync(cafeId, tier, cancellationToken); + return Ok(new ApiResponse(true, data)); + } + + [HttpPost("items/{id}/ai-3d")] + public async Task GenerateAi3d( + string cafeId, + string id, + ITenantContext tenant, + CancellationToken cancellationToken) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var tier = tenant.PlanTier ?? PlanTier.Free; + var (data, code, message) = await _menuAi3d.GenerateFromItemImageAsync(cafeId, id, tier, cancellationToken); + if (code is not null) + { + return code switch + { + "NOT_FOUND" => NotFound(new ApiResponse(false, null, new ApiError(code, message ?? ""))), + "PLAN_FEATURE_DISABLED" => StatusCode( + StatusCodes.Status403Forbidden, + new ApiResponse(false, null, new ApiError(code, message ?? ""))), + "PLAN_LIMIT_REACHED" => StatusCode( + StatusCodes.Status403Forbidden, + new ApiResponse(false, null, new ApiError(code, message ?? ""))), + _ => BadRequest(new ApiResponse(false, null, new ApiError(code, message ?? ""))) + }; + } + + return Ok(new ApiResponse(true, data)); + } +} diff --git a/src/Meezi.API/Controllers/NotificationsController.cs b/src/Meezi.API/Controllers/NotificationsController.cs new file mode 100644 index 0000000..d6f6e70 --- /dev/null +++ b/src/Meezi.API/Controllers/NotificationsController.cs @@ -0,0 +1,51 @@ +using Microsoft.AspNetCore.Mvc; +using Meezi.API.Models.Notifications; +using Meezi.API.Services; +using Meezi.Core.Interfaces; +using Meezi.Shared; + +namespace Meezi.API.Controllers; + +[Route("api/cafes/{cafeId}/notifications")] +public class NotificationsController : CafeApiControllerBase +{ + private readonly INotificationInboxService _inbox; + + public NotificationsController(INotificationInboxService inbox) + { + _inbox = inbox; + } + + [HttpGet] + public async Task List( + string cafeId, + ITenantContext tenant, + [FromQuery] bool unreadOnly = false, + [FromQuery] int limit = 40, + CancellationToken ct = default) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var data = await _inbox.ListAsync(cafeId, unreadOnly, limit, ct); + return Ok(new ApiResponse(true, data)); + } + + [HttpGet("unread-count")] + public async Task UnreadCount(string cafeId, ITenantContext tenant, CancellationToken ct = default) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var count = await _inbox.GetUnreadCountAsync(cafeId, ct); + return Ok(new ApiResponse(true, new { count })); + } + + [HttpPost("read")] + public async Task MarkRead( + string cafeId, + [FromBody] MarkNotificationsReadRequest request, + ITenantContext tenant, + CancellationToken ct = default) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + await _inbox.MarkReadAsync(cafeId, request, ct); + return Ok(new ApiResponse(true, new { read = true })); + } +} diff --git a/src/Meezi.API/Controllers/OrdersController.cs b/src/Meezi.API/Controllers/OrdersController.cs new file mode 100644 index 0000000..9bd1678 --- /dev/null +++ b/src/Meezi.API/Controllers/OrdersController.cs @@ -0,0 +1,235 @@ +using FluentValidation; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Meezi.API.Models.Orders; +using Meezi.API.Services; +using Meezi.Core.Enums; +using Meezi.Core.Interfaces; +using Meezi.Shared; + +namespace Meezi.API.Controllers; + +[Route("api/cafes/{cafeId}/orders")] +public class OrdersController : CafeApiControllerBase +{ + private readonly IOrderService _orderService; + private readonly IValidator _createValidator; + private readonly IValidator _statusValidator; + private readonly IValidator _paymentsValidator; + private readonly IValidator _appendValidator; + private readonly IValidator _sessionValidator; + + public OrdersController( + IOrderService orderService, + IValidator createValidator, + IValidator statusValidator, + IValidator paymentsValidator, + IValidator appendValidator, + IValidator sessionValidator) + { + _orderService = orderService; + _createValidator = createValidator; + _statusValidator = statusValidator; + _paymentsValidator = paymentsValidator; + _appendValidator = appendValidator; + _sessionValidator = sessionValidator; + } + + [HttpGet] + public async Task GetOrders( + string cafeId, + [FromQuery] OrderStatus? status, + ITenantContext tenant, + CancellationToken cancellationToken) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var data = await _orderService.GetOrdersAsync(cafeId, status, cancellationToken); + return Ok(new ApiResponse>(true, data)); + } + + [HttpGet("open")] + public async Task GetOpenOrders( + string cafeId, + [FromQuery] string? search, + ITenantContext tenant, + CancellationToken cancellationToken) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var data = await _orderService.GetOpenOrdersAsync(cafeId, search, cancellationToken); + return Ok(new ApiResponse>(true, data)); + } + + [HttpGet("live")] + public async Task GetLiveOrders(string cafeId, ITenantContext tenant, CancellationToken cancellationToken) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var data = await _orderService.GetLiveOrdersAsync(cafeId, cancellationToken); + return Ok(new ApiResponse>(true, data)); + } + + [HttpGet("{id}")] + public async Task GetOrder(string cafeId, string id, ITenantContext tenant, CancellationToken cancellationToken) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var data = await _orderService.GetOrderAsync(cafeId, id, cancellationToken); + if (data is null) return NotFoundError(); + return Ok(new ApiResponse(true, data)); + } + + [HttpPost] + public async Task CreateOrder( + string cafeId, + [FromBody] CreateOrderRequest request, + ITenantContext tenant, + CancellationToken cancellationToken) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var validation = await _createValidator.ValidateAsync(request, cancellationToken); + if (!validation.IsValid) return BadRequest(ValidationError(validation)); + + var result = await _orderService.CreateOrderAsync(cafeId, tenant, request, cancellationToken); + if (!result.Success) + return OrderError(result.ErrorCode!, result.Field); + + return Ok(new ApiResponse(true, result.Data)); + } + + [HttpPost("{id}/items")] + public async Task AppendItems( + string cafeId, + string id, + [FromBody] AppendOrderItemsRequest request, + ITenantContext tenant, + CancellationToken cancellationToken) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var validation = await _appendValidator.ValidateAsync(request, cancellationToken); + if (!validation.IsValid) return BadRequest(ValidationError(validation)); + + var result = await _orderService.AppendOrderItemsAsync(cafeId, id, request, cancellationToken); + if (!result.Success) + return OrderError(result.ErrorCode!, result.Field); + + return Ok(new ApiResponse(true, result.Data)); + } + + [HttpPatch("{id}/items/{itemId}/void")] + [Authorize(Roles = "Manager,Owner")] + public async Task VoidOrderItem( + string cafeId, + string id, + string itemId, + ITenantContext tenant, + CancellationToken cancellationToken) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (string.IsNullOrEmpty(tenant.UserId)) + return StatusCode(StatusCodes.Status403Forbidden, + new ApiResponse(false, null, new ApiError("FORBIDDEN", "User context required."))); + + var result = await _orderService.VoidOrderItemAsync(cafeId, id, itemId, tenant.UserId, cancellationToken); + if (!result.Success) + return OrderError(result.ErrorCode!, result.Field); + + return Ok(new ApiResponse(true, result.Data)); + } + + [HttpPost("{id}/transfer")] + [Authorize(Roles = "Manager,Owner,Waiter")] + public async Task TransferTable( + string cafeId, + string id, + [FromBody] TransferTableRequest request, + ITenantContext tenant, + CancellationToken cancellationToken) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + + var result = await _orderService.TransferTableAsync(cafeId, id, request.TargetTableId, cancellationToken); + if (!result.Success) + return OrderError(result.ErrorCode!, result.Field); + + return Ok(new ApiResponse(true, result.Data)); + } + + [HttpPatch("{id}/session")] + public async Task UpdateSession( + string cafeId, + string id, + [FromBody] UpdateOrderSessionRequest request, + ITenantContext tenant, + CancellationToken cancellationToken) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var validation = await _sessionValidator.ValidateAsync(request, cancellationToken); + if (!validation.IsValid) return BadRequest(ValidationError(validation)); + + var result = await _orderService.UpdateOrderSessionAsync(cafeId, id, request, cancellationToken); + if (!result.Success) + return OrderError(result.ErrorCode!, result.Field); + + return Ok(new ApiResponse(true, result.Data)); + } + + [HttpPatch("{id}/status")] + public async Task UpdateStatus( + string cafeId, + string id, + [FromBody] UpdateOrderStatusRequest request, + ITenantContext tenant, + CancellationToken cancellationToken) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var validation = await _statusValidator.ValidateAsync(request, cancellationToken); + if (!validation.IsValid) return BadRequest(ValidationError(validation)); + + var data = await _orderService.UpdateStatusAsync(cafeId, id, request.Status, cancellationToken); + if (data is null) return NotFoundError(); + return Ok(new ApiResponse(true, data)); + } + + [HttpPost("{id}/payments")] + public async Task RecordPayments( + string cafeId, + string id, + [FromBody] RecordPaymentsRequest request, + ITenantContext tenant, + CancellationToken cancellationToken) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var validation = await _paymentsValidator.ValidateAsync(request, cancellationToken); + if (!validation.IsValid) return BadRequest(ValidationError(validation)); + + var result = await _orderService.RecordPaymentsAsync( + cafeId, id, request, tenant.UserId, cancellationToken); + if (!result.Success) return OrderError(result.ErrorCode!, result.Field); + return Ok(new ApiResponse>(true, result.Data)); + } + + private IActionResult OrderError(string code, string? field = null) => + code switch + { + "TABLE_NOT_AVAILABLE" => BadRequest(new ApiResponse( + false, null, new ApiError(code, "Table is not available for new orders.", field))), + "TABLE_OCCUPIED" => Conflict(new ApiResponse( + false, null, new ApiError(code, "Table already has an active order.", field))), + "ORDER_NOT_OPEN" => BadRequest(new ApiResponse( + false, null, new ApiError(code, "Order is not open for changes.", field))), + "ORDER_NOT_FOUND" => NotFound(new ApiResponse( + false, null, new ApiError(code, "Order not found.", field))), + "ORDER_ALREADY_CLOSED" => BadRequest(new ApiResponse( + false, null, new ApiError(code, "Order is already closed.", field))), + "ITEM_NOT_FOUND" => NotFound(new ApiResponse( + false, null, new ApiError(code, "Line item not found.", field))), + "ITEM_ALREADY_VOIDED" => BadRequest(new ApiResponse( + false, null, new ApiError(code, "Line item is already voided.", field))), + "TABLE_NOT_FOUND" => NotFound(new ApiResponse( + false, null, new ApiError(code, "Table not found.", field))), + "TABLE_CLEANING" => BadRequest(new ApiResponse( + false, null, new ApiError(code, "Table is being cleaned.", field))), + "NO_OPEN_SHIFT" => BadRequest(new ApiResponse( + false, null, new ApiError(code, "Open the cash register shift before taking payment.", field))), + _ => BadRequest(new ApiResponse( + false, null, new ApiError(code, "Invalid order request.", field))) + }; +} diff --git a/src/Meezi.API/Controllers/PosDeviceController.cs b/src/Meezi.API/Controllers/PosDeviceController.cs new file mode 100644 index 0000000..ae86f6d --- /dev/null +++ b/src/Meezi.API/Controllers/PosDeviceController.cs @@ -0,0 +1,53 @@ +using FluentValidation; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Meezi.API.Models.Printing; +using Meezi.API.Services; +using Meezi.Core.Interfaces; +using Meezi.Shared; + +namespace Meezi.API.Controllers; + +[Route("api/cafes/{cafeId}/branches/{branchId}/pos-device")] +[Authorize(Roles = "Cashier,Manager,Owner")] +public class PosDeviceController : CafeApiControllerBase +{ + private readonly IPosDeviceService _posDevice; + private readonly IValidator _validator; + + public PosDeviceController(IPosDeviceService posDevice, IValidator validator) + { + _posDevice = posDevice; + _validator = validator; + } + + [HttpPost("payment-request")] + public async Task RequestPayment( + string cafeId, + string branchId, + [FromBody] PosPaymentRequest request, + ITenantContext tenant, + CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + + var validation = await _validator.ValidateAsync(request, ct); + if (!validation.IsValid) return BadRequest(ValidationError(validation)); + + var result = await _posDevice.SendPaymentRequestAsync(cafeId, branchId, request, ct); + + if (!result.Success) + { + return BadRequest(new ApiResponse( + false, + null, + new ApiError( + result.ErrorCode ?? "POS_DEVICE_FAILED", + result.Detail ?? "Could not send amount to POS device."))); + } + + return Ok(new ApiResponse( + true, + new PosPaymentResultDto(!result.Skipped, result.Skipped, null))); + } +} diff --git a/src/Meezi.API/Controllers/PrintController.cs b/src/Meezi.API/Controllers/PrintController.cs new file mode 100644 index 0000000..9c8742c --- /dev/null +++ b/src/Meezi.API/Controllers/PrintController.cs @@ -0,0 +1,82 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Meezi.API.Models.Printing; +using Meezi.API.Services.Printing; +using Meezi.Core.Interfaces; +using Meezi.Shared; + +namespace Meezi.API.Controllers; + +[Route("api/cafes/{cafeId}/print")] +public class PrintController : CafeApiControllerBase +{ + private readonly IPrinterService _printer; + + public PrintController(IPrinterService printer) => _printer = printer; + + [HttpPost("receipt/{orderId}")] + public async Task PrintReceipt( + string cafeId, + string orderId, + ITenantContext tenant, + CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + + var result = await _printer.PrintReceiptAsync(cafeId, orderId, ct); + return ToActionResult(result); + } + + [HttpPost("kitchen/{orderId}")] + public async Task PrintKitchen( + string cafeId, + string orderId, + ITenantContext tenant, + CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + + var result = await _printer.PrintKitchenTicketAsync(cafeId, orderId, ct); + return ToActionResult(result); + } + + [HttpPost("test")] + [Authorize(Roles = "Manager,Owner")] + public async Task TestPrint( + string cafeId, + [FromBody] TestPrintRequest request, + ITenantContext tenant, + CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + + var result = await _printer.TestPrintAsync(request.PrinterIp, request.Port, ct); + return ToActionResult(result); + } + + private IActionResult ToActionResult(PrintResult result) + { + if (result.Success) + return Ok(new ApiResponse(true, + new PrintJobResultDto(true, null, null))); + + var status = result.ErrorCode switch + { + "PRINTER_NOT_CONFIGURED" or "KITCHEN_PRINTER_NOT_CONFIGURED" => StatusCodes.Status400BadRequest, + "ORDER_NOT_FOUND" => StatusCodes.Status404NotFound, + _ => StatusCodes.Status502BadGateway + }; + + return StatusCode(status, new ApiResponse(false, null, + new ApiError(result.ErrorCode!, MessageForCode(result.ErrorCode), null))); + } + + private static string MessageForCode(string? code) => code switch + { + "PRINTER_NOT_CONFIGURED" => "Receipt printer IP is not configured for this branch.", + "KITCHEN_PRINTER_NOT_CONFIGURED" => "Kitchen printer IP is not configured for this branch.", + "PRINTER_CONNECTION_FAILED" => "Could not connect to the printer.", + "ORDER_NOT_FOUND" => "Order not found.", + _ => "Print failed." + }; +} diff --git a/src/Meezi.API/Controllers/PublicCoffeeAdvisorController.cs b/src/Meezi.API/Controllers/PublicCoffeeAdvisorController.cs new file mode 100644 index 0000000..9a3bd1c --- /dev/null +++ b/src/Meezi.API/Controllers/PublicCoffeeAdvisorController.cs @@ -0,0 +1,53 @@ +using FluentValidation; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.RateLimiting; +using Meezi.API.Models.Public; +using Meezi.API.Services; +using Meezi.Shared; + +namespace Meezi.API.Controllers; + +[ApiController] +[Route("api/public/coffee-advisor")] +public class PublicCoffeeAdvisorController : ControllerBase +{ + private readonly ICoffeeAdvisorService _advisor; + private readonly IValidator _validator; + + public PublicCoffeeAdvisorController( + ICoffeeAdvisorService advisor, + IValidator validator) + { + _advisor = advisor; + _validator = validator; + } + + [HttpPost] + [EnableRateLimiting("public-write")] + public async Task Recommend( + [FromBody] CoffeeAdvisorRequest request, + CancellationToken cancellationToken) + { + var validation = await _validator.ValidateAsync(request, cancellationToken); + if (!validation.IsValid) + { + var first = validation.Errors[0]; + return BadRequest(new ApiResponse(false, null, + new ApiError("VALIDATION_ERROR", first.ErrorMessage, first.PropertyName))); + } + + var (data, code, message) = await _advisor.RecommendAsync(request, cancellationToken); + if (data is null) + { + return code switch + { + "AI_NOT_CONFIGURED" => StatusCode(503, new ApiResponse(false, null, + new ApiError(code, message ?? "Advisor unavailable."))), + _ => BadRequest(new ApiResponse(false, null, + new ApiError(code ?? "AI_FAILED", message ?? "Request failed."))) + }; + } + + return Ok(new ApiResponse(true, data)); + } +} diff --git a/src/Meezi.API/Controllers/PublicController.cs b/src/Meezi.API/Controllers/PublicController.cs new file mode 100644 index 0000000..699380a --- /dev/null +++ b/src/Meezi.API/Controllers/PublicController.cs @@ -0,0 +1,370 @@ +using FluentValidation; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.RateLimiting; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; +using Meezi.API.Models.Public; +using Meezi.API.Models.Queue; +using Meezi.API.Security; +using Meezi.API.Services; +using Meezi.Infrastructure.Data; +using Meezi.Shared; +using Microsoft.EntityFrameworkCore; + +namespace Meezi.API.Controllers; + +[ApiController] +[AllowAnonymous] +[Route("api/public")] +public class PublicController : ControllerBase +{ + private readonly IPublicService _public; + private readonly IReviewService _reviews; + private readonly IValidator _orderValidator; + private readonly IValidator _qrOrderValidator; + private readonly IValidator _reservationValidator; + private readonly IValidator _reviewValidator; + private readonly IAbuseProtectionService _abuse; + private readonly AbuseProtectionOptions _securityOptions; + + public PublicController( + IPublicService publicService, + IReviewService reviews, + IValidator orderValidator, + IValidator qrOrderValidator, + IValidator reservationValidator, + IValidator reviewValidator, + IAbuseProtectionService abuse, + IOptions securityOptions) + { + _public = publicService; + _reviews = reviews; + _orderValidator = orderValidator; + _qrOrderValidator = qrOrderValidator; + _reservationValidator = reservationValidator; + _reviewValidator = reviewValidator; + _abuse = abuse; + _securityOptions = securityOptions.Value; + } + + [HttpGet("security-config")] + [EnableRateLimiting("public-read")] + public IActionResult GetSecurityConfig() + { + var dto = new PublicSecurityConfigDto( + _securityOptions.Enabled, + _abuse.CaptchaSiteKey, + _securityOptions.RequireCaptchaOnPublicWrites && _abuse.IsCaptchaConfigured); + return Ok(new ApiResponse(true, dto)); + } + + [HttpGet("discover")] + [EnableRateLimiting("public-read")] + public async Task Discover( + [FromQuery] string? city, + [FromQuery] string? q, + [FromQuery] double? minRating, + [FromQuery] string? sort, + [FromQuery] string? themes, + [FromQuery] string? vibes, + [FromQuery] string? occasions, + [FromQuery] string? spaceFeatures, + [FromQuery] string? noise, + [FromQuery] string? priceTier, + [FromQuery] string? size, + [FromQuery] bool requireProfile = true, + [FromQuery] bool openNow = false, + CancellationToken ct = default) + { + var filters = DiscoverFilterParams.FromQuery( + city, q, minRating, sort, themes, vibes, occasions, spaceFeatures, + noise, priceTier, size, requireProfile, openNow); + var data = await _public.DiscoverAsync(filters, ct); + return Ok(new ApiResponse>(true, data)); + } + + [HttpGet("cafes/{slug}/reviews")] + [EnableRateLimiting("public-read")] + public async Task GetReviews( + string slug, + [FromQuery] int page = 1, + [FromQuery] int pageSize = 20, + CancellationToken ct = default) + { + var cafe = await _public.GetCafeAsync(slug, ct); + if (cafe is null) return NotFound(new ApiResponse(false, null, new ApiError("NOT_FOUND", "Cafe not found."))); + var data = await _reviews.GetReviewsAsync(cafe.Id, page, pageSize, publicOnly: true, ct); + return Ok(new ApiResponse>(true, data)); + } + + [HttpPost("cafes/{slug}/reviews")] + public async Task CreateReview( + string slug, + [FromBody] CreateCafeReviewRequest request, + CancellationToken ct) + { + var validation = await _reviewValidator.ValidateAsync(request, ct); + if (!validation.IsValid) + { + var first = validation.Errors.First(); + return BadRequest(new ApiResponse(false, null, new ApiError("VALIDATION_ERROR", first.ErrorMessage, first.PropertyName))); + } + + var cafe = await _public.GetCafeAsync(slug, ct); + if (cafe is null) return NotFound(new ApiResponse(false, null, new ApiError("NOT_FOUND", "Cafe not found."))); + + var (data, code, message) = await _reviews.CreateReviewAsync(cafe.Id, request, ct); + if (data is null) + { + return StatusCode( + PublicWriteStatusCodes.ToHttpStatus(code), + new ApiResponse(false, null, new ApiError(code ?? "ERROR", message ?? "Could not submit review."))); + } + + return Ok(new ApiResponse(true, data)); + } + + [HttpPost("cafes/{slug}/reviews/upload")] + [RequestSizeLimit(20 * 1024 * 1024)] + public async Task CreateReviewWithPhotos( + string slug, + [FromForm] string authorName, + [FromForm] int rating, + [FromForm] string? comment, + [FromForm] string? authorPhone, + [FromForm] string? captchaToken, + [FromForm] List? photos, + CancellationToken ct) + { + var request = new CreateCafeReviewRequest( + authorName?.Trim() ?? "", + authorPhone?.Trim(), + rating, + comment?.Trim(), + captchaToken); + + var validation = await _reviewValidator.ValidateAsync(request, ct); + if (!validation.IsValid) + { + var first = validation.Errors.First(); + return BadRequest(new ApiResponse(false, null, new ApiError("VALIDATION_ERROR", first.ErrorMessage, first.PropertyName))); + } + + var cafe = await _public.GetCafeAsync(slug, ct); + if (cafe is null) return NotFound(new ApiResponse(false, null, new ApiError("NOT_FOUND", "Cafe not found."))); + + var (data, code, message) = await _reviews.CreateReviewWithPhotosAsync( + cafe.Id, + request, + photos ?? [], + ct); + if (data is null) + { + return StatusCode( + PublicWriteStatusCodes.ToHttpStatus(code), + new ApiResponse(false, null, new ApiError(code ?? "ERROR", message ?? "Could not submit review."))); + } + + return Ok(new ApiResponse(true, data)); + } + + [HttpGet("badge-catalog")] + [EnableRateLimiting("public-read")] + public IActionResult BadgeCatalog() + { + var data = Core.Discover.CafeBadgeCatalog.All + .Select(b => new CafeBadgePublicDto(b.Key, b.LabelFa, b.Icon)) + .ToList(); + return Ok(new ApiResponse>(true, data)); + } + + [HttpGet("cafes/{slug}")] + [EnableRateLimiting("public-read")] + public async Task GetCafe(string slug, CancellationToken ct) + { + var data = await _public.GetCafeAsync(slug, ct); + if (data is null) return NotFound(new ApiResponse(false, null, new ApiError("NOT_FOUND", "Cafe not found."))); + return Ok(new ApiResponse(true, data)); + } + + [HttpGet("cafes/{slug}/menu")] + [EnableRateLimiting("public-read")] + public async Task GetMenu(string slug, CancellationToken ct) + { + var data = await _public.GetMenuAsync(slug, ct); + if (data is null) return NotFound(new ApiResponse(false, null, new ApiError("NOT_FOUND", "Cafe not found."))); + return Ok(new ApiResponse(true, data)); + } + + [HttpPost("cafes/{slug}/orders")] + public async Task PlaceOrder( + string slug, + [FromBody] GuestCreateOrderRequest request, + CancellationToken ct) + { + var validation = await _orderValidator.ValidateAsync(request, ct); + if (!validation.IsValid) + { + var first = validation.Errors.First(); + return BadRequest(new ApiResponse(false, null, new ApiError("VALIDATION_ERROR", first.ErrorMessage, first.PropertyName))); + } + + var (data, code, message) = await _public.PlaceOrderAsync(slug, request, ct); + if (data is null) + { + return StatusCode( + PublicWriteStatusCodes.ToHttpStatus(code), + new ApiResponse(false, null, new ApiError(code ?? "ERROR", message ?? "Failed."))); + } + + return Ok(new ApiResponse(true, data)); + } + + [HttpGet("orders/{orderId}/track")] + [EnableRateLimiting("public-read")] + public async Task TrackOrder( + string orderId, + [FromQuery] string token, + CancellationToken ct) + { + if (string.IsNullOrWhiteSpace(token)) + return BadRequest(new ApiResponse(false, null, new ApiError("VALIDATION_ERROR", "Tracking token is required."))); + + var data = await _public.TrackOrderAsync(orderId, token, ct); + if (data is null) return NotFound(new ApiResponse(false, null, new ApiError("NOT_FOUND", "Order not found."))); + return Ok(new ApiResponse(true, data)); + } + + [HttpGet("{cafeId}/branches/{branchId}/menu")] + [EnableRateLimiting("public-read")] + public async Task GetBranchMenu( + string cafeId, + string branchId, + CancellationToken ct) + { + var data = await _public.GetBranchMenuAsync(cafeId, branchId, ct); + if (data is null) + return NotFound(new ApiResponse(false, null, new ApiError("NOT_FOUND", "Branch or menu not found."))); + return Ok(new ApiResponse(true, data)); + } + + [HttpGet("{cafeId}/branches/{branchId}/identity")] + [EnableRateLimiting("public-read")] + public async Task GetBranchIdentity( + string cafeId, + string branchId, + [FromServices] IBranchIdentityService identity, + CancellationToken ct) + { + var data = await identity.GetEffectiveIdentityAsync(cafeId, branchId, ct); + if (data is null) + return NotFound(new ApiResponse(false, null, new ApiError("NOT_FOUND", "Branch not found."))); + return Ok(new ApiResponse(true, data)); + } + + [HttpPost("{cafeId}/branches/{branchId}/orders")] + public async Task PlaceBranchGuestOrder( + string cafeId, + string branchId, + [FromBody] PlaceGuestOrderRequest request, + CancellationToken ct) + { + var validation = await _qrOrderValidator.ValidateAsync(request, ct); + if (!validation.IsValid) + { + var first = validation.Errors.First(); + return BadRequest(new ApiResponse(false, null, + new ApiError("VALIDATION_ERROR", first.ErrorMessage, first.PropertyName))); + } + + var (data, code, message) = await _public.PlaceBranchGuestOrderAsync(cafeId, branchId, request, ct); + if (data is null) + { + return StatusCode( + PublicWriteStatusCodes.ToHttpStatus(code), + new ApiResponse(false, null, new ApiError(code ?? "ERROR", message ?? "Failed."))); + } + + return Ok(new ApiResponse(true, data)); + } + + [HttpPost("cafes/{slug}/reservations")] + public async Task CreateReservation( + string slug, + [FromBody] CreateReservationRequest request, + CancellationToken ct) + { + var validation = await _reservationValidator.ValidateAsync(request, ct); + if (!validation.IsValid) + { + var first = validation.Errors.First(); + return BadRequest(new ApiResponse(false, null, new ApiError("VALIDATION_ERROR", first.ErrorMessage, first.PropertyName))); + } + + var (data, code, message) = await _public.CreateReservationAsync(slug, request, ct); + if (data is null) + { + return StatusCode( + PublicWriteStatusCodes.ToHttpStatus(code), + new ApiResponse(false, null, new ApiError(code ?? "ERROR", message ?? "Failed."))); + } + + return Ok(new ApiResponse(true, data)); + } + + [HttpPost("cafes/{slug}/queue/tickets")] + [EnableRateLimiting("public-write")] + public async Task IssuePublicQueueTicket( + string slug, + [FromBody] IssueQueueTicketRequest request, + [FromServices] IQueueService queue, + [FromServices] AppDbContext db, + CancellationToken ct) + { + var cafe = await db.Cafes.AsNoTracking() + .FirstOrDefaultAsync(c => c.Slug == slug && c.DeletedAt == null, ct); + if (cafe is null) + return NotFound(new ApiResponse(false, null, new ApiError("NOT_FOUND", "Cafe not found."))); + + var (ticket, code, message) = await queue.IssuePublicAsync(cafe.Id, cafe.PlanTier, request, ct); + if (ticket is null) + { + var status = code == "PLAN_LIMIT_REACHED" ? 403 : 400; + return StatusCode(status, new ApiResponse(false, null, + new ApiError(code ?? "ERROR", message ?? "Could not issue ticket."))); + } + + return Ok(new ApiResponse(true, ticket)); + } + + [HttpPost("{cafeId}/tables/{tableId}/call-waiter")] + [EnableRateLimiting("public-read")] + public async Task CallWaiter( + string cafeId, + string tableId, + [FromServices] AppDbContext db, + [FromServices] IOrderNotificationService notifications, + [FromServices] IMemoryCache cache, + CancellationToken ct) + { + var cooldownKey = $"call-waiter:{cafeId}:{tableId}"; + if (cache.TryGetValue(cooldownKey, out _)) + return StatusCode(StatusCodes.Status429TooManyRequests, + new ApiResponse(false, null, new ApiError("RATE_LIMITED", "Please wait 60 seconds before calling again."))); + + var table = await db.Tables.AsNoTracking() + .Where(t => t.Id == tableId && t.CafeId == cafeId && t.DeletedAt == null && t.IsActive) + .Select(t => new { t.Id, t.Number }) + .FirstOrDefaultAsync(ct); + + if (table is null) + return NotFound(new ApiResponse(false, null, new ApiError("NOT_FOUND", "Table not found."))); + + cache.Set(cooldownKey, true, TimeSpan.FromSeconds(60)); + + await notifications.NotifyCallWaiterAsync(cafeId, tableId, table.Number, ct); + + return Ok(new ApiResponse(true, null)); + } +} diff --git a/src/Meezi.API/Controllers/PublicDiscoverController.cs b/src/Meezi.API/Controllers/PublicDiscoverController.cs new file mode 100644 index 0000000..8b884ac --- /dev/null +++ b/src/Meezi.API/Controllers/PublicDiscoverController.cs @@ -0,0 +1,44 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.RateLimiting; +using Meezi.API.Models.Discover; +using Meezi.API.Models.Public; +using Meezi.API.Services; +using Meezi.Shared; + +namespace Meezi.API.Controllers; + +[ApiController] +[AllowAnonymous] +[Route("api/public")] +public class PublicDiscoverController : ControllerBase +{ + [HttpGet("discover-profile/taxonomy")] + [EnableRateLimiting("public-read")] + public IActionResult Taxonomy() => + Ok(new ApiResponse(true, CafeDiscoverProfileMapping.Taxonomy())); + + /// + /// Parse a free-text Persian query and return the filter hints the NLP engine detected. + /// Used by the frontend to show "detected filters" chips under the AI search box. + /// + [HttpGet("discover/nlp-parse")] + [EnableRateLimiting("public-read")] + public IActionResult NlpParse([FromQuery] string q) + { + if (string.IsNullOrWhiteSpace(q)) + return Ok(new ApiResponse(true, DiscoverNlpHintsDto.Empty)); + + var hints = DiscoverNlpParser.Parse(q); + var dto = new DiscoverNlpHintsDto( + hints.Themes, + hints.Vibes, + hints.Occasions, + hints.SpaceFeatures, + hints.NoiseLevel, + hints.PriceTier, + hints.Size); + + return Ok(new ApiResponse(true, dto)); + } +} diff --git a/src/Meezi.API/Controllers/QrController.cs b/src/Meezi.API/Controllers/QrController.cs new file mode 100644 index 0000000..cfe5fe2 --- /dev/null +++ b/src/Meezi.API/Controllers/QrController.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.RateLimiting; +using Meezi.API.Models.Tables; +using Meezi.API.Services; +using Meezi.Shared; + +namespace Meezi.API.Controllers; + +[ApiController] +[AllowAnonymous] +[Route("api/q")] +public class QrController : ControllerBase +{ + private readonly ITableService _tableService; + + public QrController(ITableService tableService) + { + _tableService = tableService; + } + + [HttpGet("{qrCode}")] + [EnableRateLimiting("public-read")] + public async Task Resolve(string qrCode, CancellationToken cancellationToken) + { + var data = await _tableService.ResolveQrAsync(qrCode, cancellationToken); + if (data is null) + return NotFound(new ApiResponse(false, null, new ApiError("NOT_FOUND", "QR code not found."))); + + return Ok(new ApiResponse(true, data)); + } +} diff --git a/src/Meezi.API/Controllers/QueueController.cs b/src/Meezi.API/Controllers/QueueController.cs new file mode 100644 index 0000000..a8bde01 --- /dev/null +++ b/src/Meezi.API/Controllers/QueueController.cs @@ -0,0 +1,88 @@ +using Microsoft.AspNetCore.Mvc; +using Meezi.API.Models.Queue; +using Meezi.API.Services; +using Meezi.Core.Enums; +using Meezi.Core.Interfaces; +using Meezi.Shared; + +namespace Meezi.API.Controllers; + +[Route("api/cafes/{cafeId}/queue")] +public class QueueController : CafeApiControllerBase +{ + private readonly IQueueService _queue; + + public QueueController(IQueueService queue) + { + _queue = queue; + } + + [HttpGet("today")] + public async Task GetToday( + string cafeId, + ITenantContext tenant, + [FromQuery] string? branchId = null, + CancellationToken ct = default) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var board = await _queue.GetTodayBoardAsync(cafeId, branchId, ct); + return Ok(new ApiResponse(true, board)); + } + + [HttpPost("next")] + public async Task IssueNext( + string cafeId, + [FromBody] IssueQueueTicketRequest request, + ITenantContext tenant, + CancellationToken ct = default) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var (ticket, error) = await _queue.IssueNextAsync(cafeId, tenant.UserId, request, ct); + if (error == "BRANCH_NOT_FOUND") + return NotFound(new ApiResponse(false, null, new ApiError(error, "Branch not found."))); + if (error == "ORDER_NOT_FOUND") + return NotFound(new ApiResponse(false, null, new ApiError(error, "Order not found."))); + return Ok(new ApiResponse(true, ticket)); + } + + [HttpPatch("{ticketId}/status")] + public async Task UpdateStatus( + string cafeId, + string ticketId, + [FromBody] UpdateQueueTicketStatusRequest request, + ITenantContext tenant, + CancellationToken ct = default) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var (ticket, error) = await _queue.UpdateStatusAsync(cafeId, ticketId, request.Status, ct); + if (error == "NOT_FOUND") + return NotFound(new ApiResponse(false, null, new ApiError(error, "Ticket not found."))); + if (error == "TICKET_EXPIRED") + return BadRequest(new ApiResponse(false, null, + new ApiError(error, "Ticket is from a previous day."))); + return Ok(new ApiResponse(true, ticket)); + } + + [HttpPost("call-next")] + public async Task CallNext( + string cafeId, + ITenantContext tenant, + [FromQuery] string? branchId = null, + CancellationToken ct = default) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var board = await _queue.GetTodayBoardAsync(cafeId, branchId, ct); + var next = board.Tickets.FirstOrDefault(t => t.Status == QueueTicketStatus.Waiting); + if (next is null) + return Ok(new ApiResponse(true, board)); + + foreach (var called in board.Tickets.Where(t => t.Status == QueueTicketStatus.Called)) + { + await _queue.UpdateStatusAsync(cafeId, called.Id, QueueTicketStatus.Done, ct); + } + + await _queue.UpdateStatusAsync(cafeId, next.Id, QueueTicketStatus.Called, ct); + var updated = await _queue.GetTodayBoardAsync(cafeId, branchId, ct); + return Ok(new ApiResponse(true, updated)); + } +} diff --git a/src/Meezi.API/Controllers/ReportsController.cs b/src/Meezi.API/Controllers/ReportsController.cs new file mode 100644 index 0000000..1fb884b --- /dev/null +++ b/src/Meezi.API/Controllers/ReportsController.cs @@ -0,0 +1,193 @@ +using Microsoft.AspNetCore.Mvc; +using Meezi.API.Models.Reports; +using Meezi.API.Services; +using Meezi.API.Utils; +using Meezi.Core.Enums; +using Meezi.Core.Interfaces; +using Meezi.Shared; + +namespace Meezi.API.Controllers; + +[Route("api/cafes/{cafeId}/reports")] +public class ReportsController : CafeApiControllerBase +{ + private readonly IReportService _reports; + private readonly IDailyReportService _dailyReports; + + public ReportsController(IReportService reports, IDailyReportService dailyReports) + { + _reports = reports; + _dailyReports = dailyReports; + } + + [HttpGet("daily")] + public async Task GetDailySnapshot( + string cafeId, + [FromQuery] string branchId, + [FromQuery] string? date, + ITenantContext tenant, + CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (string.IsNullOrWhiteSpace(branchId)) + return BadRequest(new ApiResponse(false, null, + new ApiError("VALIDATION_ERROR", "branchId is required.", "branchId"))); + + if (!TryParseReportDate(date, out var reportDate)) + return BadRequest(new ApiResponse(false, null, + new ApiError("VALIDATION_ERROR", "Invalid date. Use yyyy-MM-dd.", "date"))); + + if (EnsureReportDateAllowed(tenant, reportDate) is { } planError) return planError; + + var snapshot = await _dailyReports.GetReportAsync(cafeId, branchId, reportDate, ct); + if (snapshot is null) + snapshot = await _dailyReports.GenerateReportAsync(cafeId, branchId, reportDate, ct); + + return Ok(new ApiResponse(true, snapshot)); + } + + [HttpGet("daily/range")] + public async Task GetDailyRange( + string cafeId, + [FromQuery] string? branchId, + [FromQuery] string from, + [FromQuery] string to, + ITenantContext tenant, + CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + + if (!TryParseReportDate(from, out var startDate) || !TryParseReportDate(to, out var endDate)) + return BadRequest(new ApiResponse(false, null, + new ApiError("VALIDATION_ERROR", "Invalid from/to. Use yyyy-MM-dd.", "from"))); + + var today = IranCalendar.TodayInIran; + var tier = tenant.PlanTier ?? PlanTier.Free; + + if (!ReportPlanGate.IsDateInRange(tier, startDate, today) + || !ReportPlanGate.IsDateInRange(tier, endDate, today)) + { + return StatusCode(403, new ApiResponse(false, null, + new ApiError("PLAN_LIMIT_REACHED", ReportPlanGate.LimitMessage(tier), "date"))); + } + + var clamped = ReportPlanGate.ClampRange(tier, startDate, endDate, today); + if (clamped is null) + return BadRequest(new ApiResponse(false, null, + new ApiError("VALIDATION_ERROR", "Invalid date range.", "from"))); + + var data = await _dailyReports.GetReportRangeAsync( + cafeId, branchId, clamped.Value.From, clamped.Value.To, ct); + + return Ok(new ApiResponse>(true, data)); + } + + [HttpGet("summary")] + public async Task GetSummary( + string cafeId, + ITenantContext tenant, + [FromQuery] int days = 30, + CancellationToken ct = default) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + + var tier = tenant.PlanTier ?? PlanTier.Free; + var maxDays = Core.Constants.PlanLimits.MaxReportHistoryDays(tier); + if (days > maxDays && maxDays != int.MaxValue) + { + return StatusCode(403, new ApiResponse(false, null, + new ApiError("PLAN_LIMIT_REACHED", ReportPlanGate.LimitMessage(tier), "days"))); + } + + days = Math.Min(days, maxDays == int.MaxValue ? 365 : maxDays); + var data = await _dailyReports.GetSummaryAsync(cafeId, days, ct); + return Ok(new ApiResponse(true, data)); + } + + [HttpGet("daily/live")] + public async Task GetDailyLive( + string cafeId, + [FromQuery] string? date, + ITenantContext tenant, + CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (date is not null && !JalaliCalendarHelper.TryParseJalaliDate(date, out _, out _, out _)) + return BadRequest(new ApiResponse(false, null, + new ApiError("VALIDATION_ERROR", "Invalid Jalali date. Use yyyy-MM-dd."))); + + var data = await _reports.GetDailyReportAsync(cafeId, date ?? string.Empty, ct); + return Ok(new ApiResponse(true, data)); + } + + [HttpGet("monthly")] + public async Task GetMonthly( + string cafeId, + [FromQuery] string? month, + ITenantContext tenant, + CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (month is not null && !JalaliCalendarHelper.TryParseJalaliMonth(month, out _, out _)) + return BadRequest(new ApiResponse(false, null, + new ApiError("VALIDATION_ERROR", "Invalid Jalali month. Use yyyy-MM."))); + + var data = await _reports.GetMonthlyReportAsync(cafeId, month ?? string.Empty, ct); + return Ok(new ApiResponse(true, data)); + } + + [HttpGet("trend")] + public async Task GetTrend( + string cafeId, + ITenantContext tenant, + [FromQuery] int days = 7, + CancellationToken ct = default) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var data = await _reports.GetTrendAsync(cafeId, days, ct); + return Ok(new ApiResponse>(true, data)); + } + + [HttpGet("export")] + public async Task Export( + string cafeId, + [FromQuery] string month, + ITenantContext tenant, + [FromQuery] string format = "excel", + CancellationToken ct = default) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (!string.Equals(format, "excel", StringComparison.OrdinalIgnoreCase)) + return BadRequest(new ApiResponse(false, null, + new ApiError("VALIDATION_ERROR", "Only excel format is supported."))); + if (!JalaliCalendarHelper.TryParseJalaliMonth(month, out _, out _)) + return BadRequest(new ApiResponse(false, null, + new ApiError("VALIDATION_ERROR", "Invalid Jalali month. Use yyyy-MM."))); + + var bytes = await _reports.ExportExcelAsync(cafeId, month, ct); + var fileName = $"meezi-report-{month}.xlsx"; + return File(bytes, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", fileName); + } + + private static bool TryParseReportDate(string? value, out DateOnly date) + { + if (string.IsNullOrWhiteSpace(value)) + { + date = IranCalendar.TodayInIran; + return true; + } + + return DateOnly.TryParse(value, out date); + } + + private IActionResult? EnsureReportDateAllowed(ITenantContext tenant, DateOnly date) + { + var tier = tenant.PlanTier ?? PlanTier.Free; + var today = IranCalendar.TodayInIran; + if (ReportPlanGate.IsDateInRange(tier, date, today)) + return null; + + return StatusCode(403, new ApiResponse(false, null, + new ApiError("PLAN_LIMIT_REACHED", ReportPlanGate.LimitMessage(tier), "date"))); + } +} diff --git a/src/Meezi.API/Controllers/ReservationsController.cs b/src/Meezi.API/Controllers/ReservationsController.cs new file mode 100644 index 0000000..a13b58d --- /dev/null +++ b/src/Meezi.API/Controllers/ReservationsController.cs @@ -0,0 +1,71 @@ +using FluentValidation; +using Microsoft.AspNetCore.Mvc; +using Meezi.API.Models.Public; +using Meezi.API.Services; +using Meezi.Core.Enums; +using Meezi.Core.Interfaces; +using Meezi.Shared; + +namespace Meezi.API.Controllers; + +[Route("api/cafes/{cafeId}/reservations")] +public class ReservationsController : CafeApiControllerBase +{ + private readonly IReservationService _reservations; + private readonly IValidator _createValidator; + + public ReservationsController( + IReservationService reservations, + IValidator createValidator) + { + _reservations = reservations; + _createValidator = createValidator; + } + + [HttpPost] + public async Task Create( + string cafeId, + [FromBody] CreateReservationRequest request, + ITenantContext tenant, + CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var validation = await _createValidator.ValidateAsync(request, ct); + if (!validation.IsValid) return BadRequest(ValidationError(validation)); + + var data = await _reservations.CreateAsync(cafeId, request, ct); + if (data is null) + return BadRequest(new ApiResponse(false, null, new ApiError("INVALID_TABLE", "Table not found."))); + + return Ok(new ApiResponse(true, data)); + } + + [HttpGet] + public async Task List( + string cafeId, + ITenantContext tenant, + [FromQuery] DateOnly? date, + [FromQuery] ReservationStatus? status, + CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var data = await _reservations.GetReservationsAsync(cafeId, date, status, ct); + return Ok(new ApiResponse>(true, data)); + } + + [HttpPatch("{id}/status")] + public async Task UpdateStatus( + string cafeId, + string id, + [FromBody] UpdateReservationStatusRequest request, + ITenantContext tenant, + CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var data = await _reservations.UpdateStatusAsync(cafeId, id, request.Status, ct); + if (data is null) return NotFoundError(); + return Ok(new ApiResponse(true, data)); + } +} + +public record UpdateReservationStatusRequest(ReservationStatus Status); diff --git a/src/Meezi.API/Controllers/ShiftsController.cs b/src/Meezi.API/Controllers/ShiftsController.cs new file mode 100644 index 0000000..420568c --- /dev/null +++ b/src/Meezi.API/Controllers/ShiftsController.cs @@ -0,0 +1,115 @@ +using FluentValidation; +using Microsoft.AspNetCore.Mvc; +using Meezi.API.Models.Shifts; +using Meezi.API.Services; +using Meezi.Core.Interfaces; +using Meezi.Shared; + +namespace Meezi.API.Controllers; + +[Route("api/cafes/{cafeId}/branches/{branchId}/shifts")] +public class ShiftsController : CafeApiControllerBase +{ + private readonly IShiftService _shifts; + private readonly IValidator _openValidator; + private readonly IValidator _closeValidator; + + public ShiftsController( + IShiftService shifts, + IValidator openValidator, + IValidator closeValidator) + { + _shifts = shifts; + _openValidator = openValidator; + _closeValidator = closeValidator; + } + + [HttpPost("open")] + public async Task Open( + string cafeId, + string branchId, + [FromBody] OpenShiftRequest request, + ITenantContext tenant, + CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (string.IsNullOrEmpty(tenant.UserId)) + return StatusCode(StatusCodes.Status401Unauthorized, + new ApiResponse(false, null, new ApiError("UNAUTHORIZED", "User context is missing."))); + + var validation = await _openValidator.ValidateAsync(request, ct); + if (!validation.IsValid) return BadRequest(ValidationError(validation)); + + var result = await _shifts.OpenShiftAsync(cafeId, branchId, request.OpeningCash, tenant.UserId, ct); + return ShiftResult(result); + } + + [HttpPost("{id}/close")] + public async Task Close( + string cafeId, + string branchId, + string id, + [FromBody] CloseShiftRequest request, + ITenantContext tenant, + CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (string.IsNullOrEmpty(tenant.UserId)) + return StatusCode(StatusCodes.Status401Unauthorized, + new ApiResponse(false, null, new ApiError("UNAUTHORIZED", "User context is missing."))); + + var validation = await _closeValidator.ValidateAsync(request, ct); + if (!validation.IsValid) return BadRequest(ValidationError(validation)); + + var result = await _shifts.CloseShiftAsync(cafeId, id, request.ClosingCash, tenant.UserId, ct); + return ShiftResult(result); + } + + [HttpGet("current")] + public async Task Current( + string cafeId, + string branchId, + ITenantContext tenant, + CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + + var data = await _shifts.GetCurrentShiftAsync(cafeId, branchId, ct); + if (data is null) + return NotFound(new ApiResponse(false, null, + new ApiError("NO_OPEN_SHIFT", "No open cash register shift for this branch."))); + + return Ok(new ApiResponse(true, data)); + } + + [HttpGet("{id}/transactions")] + public async Task Transactions( + string cafeId, + string branchId, + string id, + ITenantContext tenant, + CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + + var data = await _shifts.GetTransactionsAsync(cafeId, id, ct); + if (data is null) return NotFoundError(); + return Ok(new ApiResponse>(true, data)); + } + + private IActionResult ShiftResult(ShiftServiceResult result) => + result.ErrorCode switch + { + null => Ok(new ApiResponse(true, result.Data)), + "SHIFT_ALREADY_OPEN" => Conflict(new ApiResponse(false, null, + new ApiError(result.ErrorCode, "This branch already has an open shift.", result.Field))), + "BRANCH_NOT_FOUND" => NotFound(new ApiResponse(false, null, + new ApiError(result.ErrorCode, "Branch not found.", result.Field))), + "SHIFT_NOT_FOUND" => NotFound(new ApiResponse(false, null, + new ApiError(result.ErrorCode, "Shift not found.", result.Field))), + "SHIFT_ALREADY_CLOSED" => BadRequest(new ApiResponse(false, null, + new ApiError(result.ErrorCode, "Shift is already closed.", result.Field))), + _ => BadRequest(new ApiResponse(false, null, + new ApiError(result.ErrorCode ?? "SHIFT_ERROR", "Shift operation failed.", result.Field))) + }; +} diff --git a/src/Meezi.API/Controllers/SmsController.cs b/src/Meezi.API/Controllers/SmsController.cs new file mode 100644 index 0000000..fc67984 --- /dev/null +++ b/src/Meezi.API/Controllers/SmsController.cs @@ -0,0 +1,65 @@ +using FluentValidation; +using Microsoft.AspNetCore.Mvc; +using Meezi.API.Models.Crm; +using Meezi.API.Services; +using Meezi.Core.Interfaces; +using Meezi.Shared; + +namespace Meezi.API.Controllers; + +[Route("api/cafes/{cafeId}/sms")] +public class SmsController : CafeApiControllerBase +{ + private readonly ISmsMarketingService _smsMarketingService; + private readonly IValidator _campaignValidator; + + public SmsController( + ISmsMarketingService smsMarketingService, + IValidator campaignValidator) + { + _smsMarketingService = smsMarketingService; + _campaignValidator = campaignValidator; + } + + [HttpGet("usage")] + public async Task GetUsage(string cafeId, ITenantContext tenant, CancellationToken cancellationToken) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (tenant.PlanTier is null) + return BadRequest(new ApiResponse(false, null, new ApiError("INVALID", "Plan tier missing."))); + + var data = await _smsMarketingService.GetUsageAsync(cafeId, tenant.PlanTier.Value, cancellationToken); + return Ok(new ApiResponse(true, data)); + } + + [HttpPost("campaign")] + public async Task SendCampaign( + string cafeId, + [FromBody] SendSmsCampaignRequest request, + ITenantContext tenant, + CancellationToken cancellationToken) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (tenant.PlanTier is null) + return BadRequest(new ApiResponse(false, null, new ApiError("INVALID", "Plan tier missing."))); + + var validation = await _campaignValidator.ValidateAsync(request, cancellationToken); + if (!validation.IsValid) return BadRequest(ValidationError(validation)); + + var (success, data, code, message) = await _smsMarketingService.SendCampaignAsync( + cafeId, tenant.PlanTier.Value, request, cancellationToken); + + if (!success) + { + return code switch + { + "PLAN_LIMIT_REACHED" => StatusCode(StatusCodes.Status403Forbidden, + new ApiResponse(false, null, new ApiError(code, message!))), + "NOT_FOUND" => NotFound(new ApiResponse(false, null, new ApiError(code, message!))), + _ => BadRequest(new ApiResponse(false, null, new ApiError(code!, message!))) + }; + } + + return Ok(new ApiResponse(true, data)); + } +} diff --git a/src/Meezi.API/Controllers/SnappfoodWebhookController.cs b/src/Meezi.API/Controllers/SnappfoodWebhookController.cs new file mode 100644 index 0000000..443be79 --- /dev/null +++ b/src/Meezi.API/Controllers/SnappfoodWebhookController.cs @@ -0,0 +1,39 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Meezi.API.Services.Delivery; +using Meezi.Core.Enums; +using Meezi.Shared; + +namespace Meezi.API.Controllers; + +[ApiController] +[AllowAnonymous] +[Route("api/webhooks/snappfood")] +public class SnappfoodWebhookController : ControllerBase +{ + private readonly IDeliveryWebhookIngressService _ingress; + + public SnappfoodWebhookController(IDeliveryWebhookIngressService ingress) => _ingress = ingress; + + [HttpPost] + public async Task Receive(CancellationToken ct) + { + using var reader = new StreamReader(Request.Body); + var rawBody = await reader.ReadToEndAsync(ct); + + var signature = Request.Headers["X-Snappfood-Signature"].FirstOrDefault() + ?? Request.Headers["X-Hub-Signature-256"].FirstOrDefault(); + + var result = await _ingress.ReceiveAsync(DeliveryPlatform.Snappfood, rawBody, signature, ct); + if (!result.Accepted) + { + var status = result.ErrorCode == "UNAUTHORIZED" + ? StatusCodes.Status401Unauthorized + : StatusCodes.Status400BadRequest; + return StatusCode(status, new ApiResponse(false, null, + new ApiError(result.ErrorCode ?? "ERROR", result.Message ?? "Failed."))); + } + + return Ok(new ApiResponse(true, new { received = true, logId = result.WebhookLogId })); + } +} diff --git a/src/Meezi.API/Controllers/SupportTicketsController.cs b/src/Meezi.API/Controllers/SupportTicketsController.cs new file mode 100644 index 0000000..d880f64 --- /dev/null +++ b/src/Meezi.API/Controllers/SupportTicketsController.cs @@ -0,0 +1,84 @@ +using Microsoft.AspNetCore.Mvc; +using Meezi.Infrastructure.Models; +using Meezi.Infrastructure.Services; +using Meezi.API.Services; +using Meezi.Core.Interfaces; +using Meezi.Shared; + +namespace Meezi.API.Controllers; + +[Route("api/cafes/{cafeId}/support/tickets")] +public class SupportTicketsController : CafeApiControllerBase +{ + private readonly ISupportTicketService _tickets; + + public SupportTicketsController(ISupportTicketService tickets) + { + _tickets = tickets; + } + + [HttpGet] + public async Task List(string cafeId, ITenantContext tenant, CancellationToken cancellationToken) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var list = await _tickets.ListForCafeAsync(cafeId, cancellationToken); + return Ok(new ApiResponse(true, list)); + } + + [HttpGet("{ticketId}")] + public async Task Get( + string cafeId, + string ticketId, + ITenantContext tenant, + CancellationToken cancellationToken) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var detail = await _tickets.GetForCafeAsync(cafeId, ticketId, cancellationToken); + if (detail is null) + return NotFoundError("Ticket not found."); + + return Ok(new ApiResponse(true, detail)); + } + + [HttpPost] + public async Task Create( + string cafeId, + [FromBody] CreateSupportTicketRequest request, + ITenantContext tenant, + CancellationToken cancellationToken) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (string.IsNullOrEmpty(tenant.UserId)) + return StatusCode(403, new ApiResponse(false, null, new ApiError("FORBIDDEN", "User required."))); + + var detail = await _tickets.CreateForCafeAsync(cafeId, tenant.UserId, request, cancellationToken); + return Ok(new ApiResponse(true, detail)); + } + + [HttpPost("{ticketId}/messages")] + public async Task Reply( + string cafeId, + string ticketId, + [FromBody] ReplySupportTicketRequest request, + ITenantContext tenant, + CancellationToken cancellationToken) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (string.IsNullOrEmpty(tenant.UserId)) + return StatusCode(403, new ApiResponse(false, null, new ApiError("FORBIDDEN", "User required."))); + + var existing = await _tickets.GetForCafeAsync(cafeId, ticketId, cancellationToken); + if (existing is null) + return NotFoundError("Ticket not found."); + if (existing.Ticket.Status is Core.Enums.SupportTicketStatus.Closed + or Core.Enums.SupportTicketStatus.Resolved) + return BadRequest(new ApiResponse(false, null, + new ApiError("TICKET_CLOSED", "This ticket is closed and cannot receive new messages."))); + + var detail = await _tickets.ReplyAsMerchantAsync(cafeId, ticketId, tenant.UserId, request, cancellationToken); + if (detail is null) + return NotFoundError("Ticket not found."); + + return Ok(new ApiResponse(true, detail)); + } +} diff --git a/src/Meezi.API/Controllers/TablesController.cs b/src/Meezi.API/Controllers/TablesController.cs new file mode 100644 index 0000000..3b7c2d2 --- /dev/null +++ b/src/Meezi.API/Controllers/TablesController.cs @@ -0,0 +1,158 @@ +using FluentValidation; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Meezi.API.Models.Orders; +using Meezi.API.Models.Tables; +using Meezi.API.Services; +using Meezi.Core.Interfaces; +using Meezi.Shared; + +namespace Meezi.API.Controllers; + +[Route("api/cafes/{cafeId}/tables")] +public class TablesController : CafeApiControllerBase +{ + private readonly ITableService _tableService; + private readonly IOrderService _orderService; + private readonly IValidator _createValidator; + private readonly IValidator _patchValidator; + private readonly IValidator _cleaningValidator; + + public TablesController( + ITableService tableService, + IOrderService orderService, + IValidator createValidator, + IValidator patchValidator, + IValidator cleaningValidator) + { + _tableService = tableService; + _orderService = orderService; + _createValidator = createValidator; + _patchValidator = patchValidator; + _cleaningValidator = cleaningValidator; + } + + [HttpGet("board")] + public async Task GetBoard( + string cafeId, + ITenantContext tenant, + [FromQuery] bool activeOnly = true, + [FromQuery] string? branchId = null, + CancellationToken ct = default) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var data = await _tableService.GetTableBoardAsync(cafeId, activeOnly, branchId, ct); + return Ok(new ApiResponse>(true, data)); + } + + [HttpGet] + public async Task GetTables( + string cafeId, + ITenantContext tenant, + [FromQuery] string? branchId = null, + CancellationToken ct = default) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var data = await _tableService.GetTablesAsync(cafeId, branchId, ct); + return Ok(new ApiResponse>(true, data)); + } + + [HttpPost] + public async Task CreateTable( + string cafeId, + [FromBody] CreateTableRequest request, + ITenantContext tenant, + CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var validation = await _createValidator.ValidateAsync(request, ct); + if (!validation.IsValid) return BadRequest(ValidationError(validation)); + + var data = await _tableService.CreateTableAsync(cafeId, request, ct); + if (data is null) return NotFoundError("Branch not found."); + return Ok(new ApiResponse(true, data)); + } + + [HttpPatch("{id}")] + public async Task PatchTable( + string cafeId, + string id, + [FromBody] PatchTableRequest request, + ITenantContext tenant, + CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var validation = await _patchValidator.ValidateAsync(request, ct); + if (!validation.IsValid) return BadRequest(ValidationError(validation)); + + var data = await _tableService.PatchTableAsync(cafeId, id, request, ct); + if (data is null) return NotFoundError(); + return Ok(new ApiResponse(true, data)); + } + + [HttpGet("{id}/active-order")] + public async Task GetActiveOrder( + string cafeId, + string id, + ITenantContext tenant, + CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var data = await _orderService.GetActiveOrderByTableAsync(cafeId, id, ct); + if (data is null) return NotFoundError("No active order for this table."); + return Ok(new ApiResponse(true, data)); + } + + [HttpDelete("{id}")] + [Authorize(Roles = "Manager,Owner")] + public async Task DeleteTable( + string cafeId, + string id, + ITenantContext tenant, + CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + + var result = await _tableService.DeleteTableAsync(cafeId, id, ct); + if (!result.Success) + { + var status = result.ErrorCode == "TABLE_HAS_OPEN_ORDER" + ? StatusCodes.Status409Conflict + : StatusCodes.Status400BadRequest; + return StatusCode(status, + new ApiResponse(false, null, new ApiError(result.ErrorCode!, result.Message ?? result.ErrorCode!))); + } + + return Ok(new ApiResponse(true, result.Data)); + } + + [HttpPatch("{id}/cleaning")] + public async Task SetCleaning( + string cafeId, + string id, + [FromBody] SetTableCleaningRequest request, + ITenantContext tenant, + CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var validation = await _cleaningValidator.ValidateAsync(request, ct); + if (!validation.IsValid) return BadRequest(ValidationError(validation)); + + var data = await _tableService.SetTableCleaningAsync(cafeId, id, request.IsCleaning, ct); + if (data is null) return NotFoundError(); + return Ok(new ApiResponse(true, data)); + } + + [HttpGet("{id}/qr")] + public async Task GetQrPng( + string cafeId, + string id, + ITenantContext tenant, + CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var png = await _tableService.GetQrPngAsync(cafeId, id, ct); + if (png is null) return NotFoundError(); + return File(png, "image/png", $"table-{id}-qr.png"); + } +} diff --git a/src/Meezi.API/Controllers/Tap30WebhookController.cs b/src/Meezi.API/Controllers/Tap30WebhookController.cs new file mode 100644 index 0000000..027f724 --- /dev/null +++ b/src/Meezi.API/Controllers/Tap30WebhookController.cs @@ -0,0 +1,39 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Meezi.API.Services.Delivery; +using Meezi.Core.Enums; +using Meezi.Shared; + +namespace Meezi.API.Controllers; + +[ApiController] +[AllowAnonymous] +[Route("api/webhooks/tap30")] +public class Tap30WebhookController : ControllerBase +{ + private readonly IDeliveryWebhookIngressService _ingress; + + public Tap30WebhookController(IDeliveryWebhookIngressService ingress) => _ingress = ingress; + + [HttpPost] + public async Task Receive(CancellationToken ct) + { + using var reader = new StreamReader(Request.Body); + var rawBody = await reader.ReadToEndAsync(ct); + + var signature = Request.Headers["X-Tap30-Signature"].FirstOrDefault() + ?? Request.Headers["X-Hub-Signature-256"].FirstOrDefault(); + + var result = await _ingress.ReceiveAsync(DeliveryPlatform.Tap30, rawBody, signature, ct); + if (!result.Accepted) + { + var status = result.ErrorCode == "UNAUTHORIZED" + ? StatusCodes.Status401Unauthorized + : StatusCodes.Status400BadRequest; + return StatusCode(status, new ApiResponse(false, null, + new ApiError(result.ErrorCode ?? "ERROR", result.Message ?? "Failed."))); + } + + return Ok(new ApiResponse(true, new { received = true, logId = result.WebhookLogId })); + } +} diff --git a/src/Meezi.API/Controllers/TarazController.cs b/src/Meezi.API/Controllers/TarazController.cs new file mode 100644 index 0000000..5897513 --- /dev/null +++ b/src/Meezi.API/Controllers/TarazController.cs @@ -0,0 +1,30 @@ +using Microsoft.AspNetCore.Mvc; +using Meezi.Core.Interfaces; +using Meezi.Shared; + +namespace Meezi.API.Controllers; + +[Route("api/cafes/{cafeId}/tax/taraz")] +public class TarazController : CafeApiControllerBase +{ + private readonly ITarazTaxService _taraz; + + public TarazController(ITarazTaxService taraz) => _taraz = taraz; + + [HttpPost("submit")] + public async Task Submit( + string cafeId, + ITenantContext tenant, + [FromQuery] DateTime? date, + CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + + var targetDate = date ?? DateTime.UtcNow.Date; + var result = await _taraz.SubmitDailyInvoicesAsync(cafeId, targetDate, ct); + if (!result.Success) + return BadRequest(new ApiResponse(false, null, new ApiError("TARAZ_ERROR", result.Message ?? "Submit failed."))); + + return Ok(new ApiResponse(true, new { trackingCode = result.TrackingCode, message = result.Message })); + } +} diff --git a/src/Meezi.API/Controllers/TaxesController.cs b/src/Meezi.API/Controllers/TaxesController.cs new file mode 100644 index 0000000..5bdad99 --- /dev/null +++ b/src/Meezi.API/Controllers/TaxesController.cs @@ -0,0 +1,65 @@ +using Microsoft.AspNetCore.Mvc; +using Meezi.API.Models.Taxes; +using Meezi.API.Services; +using Meezi.Core.Interfaces; +using Meezi.Shared; + +namespace Meezi.API.Controllers; + +[Route("api/cafes/{cafeId}/taxes")] +public class TaxesController : CafeApiControllerBase +{ + private readonly ITaxService _taxService; + + public TaxesController(ITaxService taxService) => _taxService = taxService; + + [HttpGet] + public async Task GetAll(string cafeId, ITenantContext tenant, CancellationToken cancellationToken) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var data = await _taxService.GetAllAsync(cafeId, cancellationToken); + return Ok(new ApiResponse>(true, data)); + } + + [HttpPost] + public async Task Create( + string cafeId, + [FromBody] CreateTaxRequest request, + ITenantContext tenant, + CancellationToken cancellationToken) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsureOwner(tenant) is { } ownerDenied) return ownerDenied; + var data = await _taxService.CreateAsync(cafeId, request, cancellationToken); + return Ok(new ApiResponse(true, data)); + } + + [HttpPatch("{id}")] + public async Task Update( + string cafeId, + string id, + [FromBody] UpdateTaxRequest request, + ITenantContext tenant, + CancellationToken cancellationToken) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsureOwner(tenant) is { } ownerDenied) return ownerDenied; + var data = await _taxService.UpdateAsync(cafeId, id, request, cancellationToken); + if (data is null) return NotFoundError(); + return Ok(new ApiResponse(true, data)); + } + + [HttpDelete("{id}")] + public async Task Delete( + string cafeId, + string id, + ITenantContext tenant, + CancellationToken cancellationToken) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsureOwner(tenant) is { } ownerDenied) return ownerDenied; + var deleted = await _taxService.DeleteAsync(cafeId, id, cancellationToken); + if (!deleted) return NotFoundError(); + return Ok(new ApiResponse(true, new { id })); + } +} diff --git a/src/Meezi.API/Controllers/TerminalsController.cs b/src/Meezi.API/Controllers/TerminalsController.cs new file mode 100644 index 0000000..7ddf253 --- /dev/null +++ b/src/Meezi.API/Controllers/TerminalsController.cs @@ -0,0 +1,55 @@ +using Microsoft.AspNetCore.Mvc; +using Meezi.Core.Constants; +using Meezi.Core.Enums; +using Meezi.Core.Interfaces; +using Meezi.API.Services; +using Meezi.Shared; + +namespace Meezi.API.Controllers; + +[Route("api/cafes/{cafeId}/terminals")] +public class TerminalsController : CafeApiControllerBase +{ + private readonly ITerminalRegistryService _terminals; + + public TerminalsController(ITerminalRegistryService terminals) => _terminals = terminals; + + [HttpPost("register")] + public async Task Register( + string cafeId, + [FromBody] RegisterTerminalRequest request, + ITenantContext tenant, + CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var tier = tenant.PlanTier ?? PlanTier.Free; + var (allowed, code, message) = await _terminals.RegisterAsync(cafeId, tier, request.TerminalId, ct); + if (!allowed) + return StatusCode(403, new ApiResponse(false, null, new ApiError(code!, message!))); + + return Ok(new ApiResponse(true, new { registered = true })); + } + + [HttpGet] + public async Task List(string cafeId, ITenantContext tenant, CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + var list = await _terminals.ListAsync(cafeId, ct); + var max = PlanLimits.MaxTerminals(tenant.PlanTier ?? PlanTier.Free); + return Ok(new ApiResponse(true, new { terminals = list, max })); + } + + [HttpDelete("{terminalId}")] + public async Task Revoke( + string cafeId, + string terminalId, + ITenantContext tenant, + CancellationToken ct) + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + await _terminals.RevokeAsync(cafeId, terminalId, ct); + return Ok(new ApiResponse(true, new { revoked = true })); + } +} + +public record RegisterTerminalRequest(string TerminalId); diff --git a/src/Meezi.API/Controllers/WebsiteContentController.cs b/src/Meezi.API/Controllers/WebsiteContentController.cs new file mode 100644 index 0000000..8eb5d86 --- /dev/null +++ b/src/Meezi.API/Controllers/WebsiteContentController.cs @@ -0,0 +1,104 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.RateLimiting; +using Meezi.Core.Interfaces; +using Meezi.Shared; + +namespace Meezi.API.Controllers; + +/// Public website endpoints — blog, comments, demo requests. +[AllowAnonymous] +[ApiController] +[Route("api/public/website")] +public class WebsiteContentController(IWebsiteService website) : ControllerBase +{ + // ── Blog posts ──────────────────────────────────────────────────────── + + [HttpGet("posts")] + public async Task GetPosts( + [FromQuery] string locale = "fa", + [FromQuery] int page = 1, + [FromQuery] int limit = 12, + CancellationToken ct = default) + { + limit = Math.Clamp(limit, 1, 50); + page = Math.Max(page, 1); + var (posts, total) = await website.GetPostsAsync(locale, page, limit, ct); + return Ok(new ApiResponse(true, new { Posts = posts, Total = total, Page = page, Limit = limit })); + } + + [HttpGet("posts/{slug}")] + public async Task GetPost(string slug, + [FromQuery] string locale = "fa", + CancellationToken ct = default) + { + var post = await website.GetPostAsync(slug, locale, ct); + if (post is null) return NotFound(new ApiResponse(false, null, + new ApiError("POST_NOT_FOUND", "Blog post not found."))); + return Ok(new ApiResponse(true, post)); + } + + // ── Comments ────────────────────────────────────────────────────────── + + [HttpGet("posts/{slug}/comments")] + public async Task GetComments(string slug, CancellationToken ct = default) + { + var comments = await website.GetCommentsAsync(slug, ct); + return Ok(new ApiResponse(true, comments)); + } + + [HttpPost("posts/{slug}/comments")] + [EnableRateLimiting("public-read")] + public async Task PostComment(string slug, + [FromBody] PostCommentRequest req, + CancellationToken ct = default) + { + if (string.IsNullOrWhiteSpace(req.AuthorName) || string.IsNullOrWhiteSpace(req.Content)) + return BadRequest(new ApiResponse(false, null, + new ApiError("VALIDATION", "AuthorName and Content are required."))); + + if (req.Content.Length > 2000) + return BadRequest(new ApiResponse(false, null, + new ApiError("VALIDATION", "Comment too long (max 2000 chars)."))); + + var ip = HttpContext.Connection.RemoteIpAddress?.ToString(); + try + { + var comment = await website.AddCommentAsync(slug, req.AuthorName, req.Email, req.Content, ip, ct); + return Ok(new ApiResponse(true, comment)); + } + catch (InvalidOperationException) + { + return NotFound(new ApiResponse(false, null, + new ApiError("POST_NOT_FOUND", "Blog post not found."))); + } + } + + // ── Demo requests ───────────────────────────────────────────────────── + + [HttpPost("demo-requests")] + [EnableRateLimiting("public-write")] + public async Task CreateDemoRequest( + [FromBody] CreateDemoRequestBody req, + CancellationToken ct = default) + { + if (string.IsNullOrWhiteSpace(req.ContactName) || + string.IsNullOrWhiteSpace(req.BusinessName) || + string.IsNullOrWhiteSpace(req.Phone)) + { + return BadRequest(new ApiResponse(false, null, + new ApiError("VALIDATION", "ContactName, BusinessName, and Phone are required."))); + } + + var result = await website.CreateDemoRequestAsync( + req.ContactName, req.BusinessName, req.Phone, + req.Email, req.BranchCount ?? "1", req.Notes, req.Source ?? "website", ct); + + return Ok(new ApiResponse(true, result)); + } +} + +public record PostCommentRequest(string AuthorName, string? Email, string Content); +public record CreateDemoRequestBody( + string ContactName, string BusinessName, string Phone, + string? Email, string? BranchCount, string? Notes, string? Source); diff --git a/src/Meezi.API/Extensions/SecurityExtensions.cs b/src/Meezi.API/Extensions/SecurityExtensions.cs new file mode 100644 index 0000000..5bdd2e8 --- /dev/null +++ b/src/Meezi.API/Extensions/SecurityExtensions.cs @@ -0,0 +1,89 @@ +using System.Threading.RateLimiting; +using Meezi.API.Security; +using Microsoft.AspNetCore.RateLimiting; + +namespace Meezi.API.Extensions; + +public static class SecurityExtensions +{ + public static IServiceCollection AddMeeziSecurity(this IServiceCollection services, IConfiguration configuration) + { + services.Configure(configuration.GetSection(AbuseProtectionOptions.SectionName)); + services.AddHttpClient("turnstile"); + services.AddSingleton(); + + services.AddRateLimiter(options => + { + options.RejectionStatusCode = StatusCodes.Status429TooManyRequests; + options.OnRejected = async (context, token) => + { + context.HttpContext.Response.ContentType = "application/json"; + await context.HttpContext.Response.WriteAsJsonAsync( + new + { + success = false, + error = new + { + code = "RATE_LIMITED", + message = "Too many requests. Please wait and try again." + } + }, + token); + }; + + options.AddPolicy("public-read", httpContext => + { + var ip = ClientIpResolver.GetClientIp(httpContext); + var limit = configuration.GetValue("Security:RateLimits:PublicReadsPerIpPerMinute", 120); + return RateLimitPartition.GetSlidingWindowLimiter( + $"read:{ip}", + _ => new SlidingWindowRateLimiterOptions + { + PermitLimit = limit, + Window = TimeSpan.FromMinutes(1), + SegmentsPerWindow = 4, + QueueProcessingOrder = QueueProcessingOrder.OldestFirst, + QueueLimit = 0 + }); + }); + + options.AddPolicy("public-write", httpContext => + { + var ip = ClientIpResolver.GetClientIp(httpContext); + var limit = configuration.GetValue("Security:RateLimits:PublicWritesPerIpPerMinute", 20); + return RateLimitPartition.GetSlidingWindowLimiter( + $"write:{ip}", + _ => new SlidingWindowRateLimiterOptions + { + PermitLimit = limit, + Window = TimeSpan.FromMinutes(1), + SegmentsPerWindow = 4, + QueueProcessingOrder = QueueProcessingOrder.OldestFirst, + QueueLimit = 0 + }); + }); + + options.AddPolicy("auth-otp", httpContext => + { + var ip = ClientIpResolver.GetClientIp(httpContext); + var limit = configuration.GetValue("Security:RateLimits:AuthOtpPerIpPerHour", 15); + return RateLimitPartition.GetFixedWindowLimiter( + $"otp:{ip}", + _ => new FixedWindowRateLimiterOptions + { + PermitLimit = limit, + Window = TimeSpan.FromHours(1), + QueueProcessingOrder = QueueProcessingOrder.OldestFirst, + QueueLimit = 0 + }); + }); + }); + + return services; + } + + public static void UseMeeziSecurity(this WebApplication app) + { + app.UseRateLimiter(); + } +} diff --git a/src/Meezi.API/Extensions/ServiceCollectionExtensions.cs b/src/Meezi.API/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..d9be6af --- /dev/null +++ b/src/Meezi.API/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,246 @@ +using System.Text; +using System.Text.Json.Serialization; +using FluentValidation; +using Hangfire; +using Hangfire.MemoryStorage; +using Hangfire.PostgreSql; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; +using Meezi.API.Hubs; +using Meezi.API.Configuration; +using Meezi.API.Jobs; +using Meezi.API.Services; +using Meezi.API.Services.Delivery; +using Meezi.Infrastructure.Services.Platform; +using Meezi.API.Services.Printing; +using Meezi.Core.Interfaces; +using Meezi.Infrastructure; +using Serilog; +using StackExchange.Redis; + +namespace Meezi.API.Extensions; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddMeeziServices(this IServiceCollection services, IConfiguration configuration) + { + services.AddHttpContextAccessor(); + services.AddMeeziSecurity(configuration); + services.AddInfrastructure(configuration); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddSingleton(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddSingleton(); + services.Configure(configuration.GetSection(MenuAi3dOptions.SectionName)); + services.AddHttpClient("MenuAi3d"); + services.AddHttpClient("OpenAi"); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddSingleton(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.Configure(configuration.GetSection(DeliveryPlatformsOptions.SectionName)); + services.PostConfigure(opts => + { + if (string.IsNullOrEmpty(opts.Snappfood.WebhookSecret)) + opts.Snappfood.WebhookSecret = configuration["Snappfood:WebhookSecret"] ?? ""; + if (string.IsNullOrEmpty(opts.Snappfood.ApiKey)) + opts.Snappfood.ApiKey = configuration["Snappfood:ApiKey"] ?? ""; + if (string.IsNullOrEmpty(opts.Snappfood.ApiBaseUrl)) + opts.Snappfood.ApiBaseUrl = configuration["Snappfood:ApiBaseUrl"] ?? ""; + }); + + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddHttpClient(nameof(PosDeviceService)); + services.AddScoped(); + services.AddScoped(); + + services.AddControllers() + .AddJsonOptions(options => + { + options.JsonSerializerOptions.PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase; + options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); + }); + services.AddEndpointsApiExplorer(); + services.AddSwaggerGen(); + services.AddSignalR(); + + services.AddValidatorsFromAssemblyContaining(); + + var jwtKey = configuration["Jwt:Key"] ?? "meezi-dev-secret-key-min-32-chars!!"; + var jwtIssuer = configuration["Jwt:Issuer"] ?? "meezi"; + var jwtAudience = configuration["Jwt:Audience"] ?? "meezi"; + + services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(options => + { + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = jwtIssuer, + ValidAudience = jwtAudience, + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey)) + }; + options.Events = new JwtBearerEvents + { + OnMessageReceived = context => + { + var accessToken = context.Request.Query["access_token"]; + var path = context.HttpContext.Request.Path; + if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/hubs")) + context.Token = accessToken; + return Task.CompletedTask; + } + }; + }); + + services.AddAuthorization(); + + var isTesting = configuration.GetValue("Testing:Enabled"); + var redisConnection = configuration.GetConnectionString("Redis") ?? "localhost:6379"; + if (isTesting) + { + services.AddSingleton(_ => + ConnectionMultiplexer.Connect($"{redisConnection},abortConnect=false,connectTimeout=500")); + } + else + { + services.AddSingleton(_ => + ConnectionMultiplexer.Connect(redisConnection)); + } + + var connectionString = configuration.GetConnectionString("DefaultConnection") + ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found."); + + services.AddHangfire(config => + { + config + .SetDataCompatibilityLevel(CompatibilityLevel.Version_180) + .UseSimpleAssemblyNameTypeSerializer() + .UseRecommendedSerializerSettings(); + + if (isTesting) + config.UseMemoryStorage(); + else + config.UsePostgreSqlStorage(options => options.UseNpgsqlConnection(connectionString)); + }); + + if (!isTesting) + services.AddHangfireServer(); + + services.AddCors(options => + { + options.AddPolicy("MeeziCors", policy => + { + var origins = configuration.GetSection("Cors:Origins").Get() + ?? ["http://localhost:3000"]; + policy.WithOrigins(origins) + .AllowAnyHeader() + .AllowAnyMethod() + .AllowCredentials(); + }); + }); + + return services; + } + + public static WebApplication ConfigureMeeziPipeline(this WebApplication app) + { + app.UseSerilogRequestLogging(); + + if (app.Environment.IsDevelopment()) + { + app.UseSwagger(); + app.UseSwaggerUI(); + } + + app.UseHttpsRedirection(); + + var uploadsPath = Path.Combine(app.Environment.ContentRootPath, "uploads"); + Directory.CreateDirectory(uploadsPath); + app.UseStaticFiles(new StaticFileOptions + { + FileProvider = new Microsoft.Extensions.FileProviders.PhysicalFileProvider(uploadsPath), + RequestPath = "/uploads" + }); + + app.UseCors("MeeziCors"); + app.UseRouting(); + app.UseMeeziSecurity(); + app.UseAuthentication(); + app.UseMiddleware(); + app.UseMiddleware(); + app.UseAuthorization(); + + app.MapControllers(); + app.MapHub("/hubs/kds"); + app.MapHub("/hubs/guest-order"); + app.MapGet("/health", () => Results.Ok(new { status = "healthy" })); + + if (!app.Configuration.GetValue("Testing:Enabled")) + { + app.UseHangfireDashboard("/hangfire"); + + RecurringJob.AddOrUpdate( + "subscription-renewal-reminder", + job => job.ExecuteAsync(), + Cron.Daily(6)); + + RecurringJob.AddOrUpdate( + "daily-reports-yesterday", + job => job.ExecuteAsync(), + "5 0 * * *", + new RecurringJobOptions { TimeZone = IranCalendar.TimeZone }); + + RecurringJob.AddOrUpdate( + "branch-permanent-delete", + job => job.ExecuteAsync(), + Cron.Hourly); + } + + return app; + } +} diff --git a/src/Meezi.API/Hubs/.gitkeep b/src/Meezi.API/Hubs/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/Meezi.API/Hubs/.gitkeep @@ -0,0 +1 @@ + diff --git a/src/Meezi.API/Hubs/GuestOrderHub.cs b/src/Meezi.API/Hubs/GuestOrderHub.cs new file mode 100644 index 0000000..7a8b7d9 --- /dev/null +++ b/src/Meezi.API/Hubs/GuestOrderHub.cs @@ -0,0 +1,33 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.SignalR; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; + +namespace Meezi.API.Hubs; + +[AllowAnonymous] +public class GuestOrderHub : Hub +{ + public static string OrderGroup(string orderId) => $"guest-order:{orderId}"; + + public async Task JoinOrder(string orderId, string trackingToken) + { + if (string.IsNullOrWhiteSpace(orderId) || string.IsNullOrWhiteSpace(trackingToken)) + throw new HubException("Invalid order."); + + var db = Context.GetHttpContext()?.RequestServices.GetRequiredService(); + if (db is null) + throw new HubException("Unavailable."); + + var valid = await db.Orders.AsNoTracking() + .AnyAsync(o => o.Id == orderId && o.GuestTrackingToken == trackingToken); + + if (!valid) + throw new HubException("Forbidden."); + + await Groups.AddToGroupAsync(Context.ConnectionId, OrderGroup(orderId)); + } + + public Task LeaveOrder(string orderId) => + Groups.RemoveFromGroupAsync(Context.ConnectionId, OrderGroup(orderId)); +} diff --git a/src/Meezi.API/Hubs/KdsHub.cs b/src/Meezi.API/Hubs/KdsHub.cs new file mode 100644 index 0000000..52e982c --- /dev/null +++ b/src/Meezi.API/Hubs/KdsHub.cs @@ -0,0 +1,23 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.SignalR; +using Meezi.Core.Constants; + +namespace Meezi.API.Hubs; + +[Authorize] +public class KdsHub : Hub +{ + public static string GroupName(string cafeId) => $"cafe:{cafeId}"; + + public async Task JoinCafe(string cafeId) + { + var claimCafeId = Context.User?.FindFirst(MeeziClaimTypes.CafeId)?.Value; + if (string.IsNullOrEmpty(claimCafeId) || claimCafeId != cafeId) + throw new HubException("Forbidden"); + + await Groups.AddToGroupAsync(Context.ConnectionId, GroupName(cafeId)); + } + + public Task LeaveCafe(string cafeId) => + Groups.RemoveFromGroupAsync(Context.ConnectionId, GroupName(cafeId)); +} diff --git a/src/Meezi.API/Jobs/.gitkeep b/src/Meezi.API/Jobs/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/Meezi.API/Jobs/.gitkeep @@ -0,0 +1 @@ + diff --git a/src/Meezi.API/Jobs/BranchPermanentDeleteJob.cs b/src/Meezi.API/Jobs/BranchPermanentDeleteJob.cs new file mode 100644 index 0000000..bd760f8 --- /dev/null +++ b/src/Meezi.API/Jobs/BranchPermanentDeleteJob.cs @@ -0,0 +1,26 @@ +using Meezi.API.Services; + +namespace Meezi.API.Jobs; + +public class BranchPermanentDeleteJob +{ + private readonly IServiceScopeFactory _scopeFactory; + private readonly ILogger _logger; + + public BranchPermanentDeleteJob( + IServiceScopeFactory scopeFactory, + ILogger logger) + { + _scopeFactory = scopeFactory; + _logger = logger; + } + + public async Task ExecuteAsync() + { + await using var scope = _scopeFactory.CreateAsyncScope(); + var lifecycle = scope.ServiceProvider.GetRequiredService(); + var purged = await lifecycle.PurgeExpiredDeletionsAsync(); + if (purged > 0) + _logger.LogInformation("Permanently deleted {Count} expired branches", purged); + } +} diff --git a/src/Meezi.API/Jobs/GenerateYesterdayReportsJob.cs b/src/Meezi.API/Jobs/GenerateYesterdayReportsJob.cs new file mode 100644 index 0000000..3fe555f --- /dev/null +++ b/src/Meezi.API/Jobs/GenerateYesterdayReportsJob.cs @@ -0,0 +1,61 @@ +using Meezi.API.Services; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; + +namespace Meezi.API.Jobs; + +public class GenerateYesterdayReportsJob +{ + private readonly IServiceScopeFactory _scopeFactory; + private readonly ILogger _logger; + + public GenerateYesterdayReportsJob( + IServiceScopeFactory scopeFactory, + ILogger logger) + { + _scopeFactory = scopeFactory; + _logger = logger; + } + + public async Task ExecuteAsync() + { + await using var scope = _scopeFactory.CreateAsyncScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var reports = scope.ServiceProvider.GetRequiredService(); + + var reportDate = IranCalendar.TodayInIran.AddDays(-1); + + var branches = await db.Branches + .Where(b => b.IsActive && b.DeletedAt == null) + .Select(b => new { b.Id, b.CafeId }) + .ToListAsync(); + + var success = 0; + var failed = 0; + + foreach (var branch in branches) + { + try + { + await reports.GenerateReportAsync(branch.CafeId, branch.Id, reportDate); + success++; + } + catch (Exception ex) + { + failed++; + _logger.LogWarning( + ex, + "Failed daily report for cafe {CafeId} branch {BranchId} date {Date}", + branch.CafeId, + branch.Id, + reportDate); + } + } + + _logger.LogInformation( + "Yesterday daily reports ({Date}): {Success} ok, {Failed} failed", + reportDate, + success, + failed); + } +} diff --git a/src/Meezi.API/Jobs/ProcessDeliveryOrderJob.cs b/src/Meezi.API/Jobs/ProcessDeliveryOrderJob.cs new file mode 100644 index 0000000..f94123d --- /dev/null +++ b/src/Meezi.API/Jobs/ProcessDeliveryOrderJob.cs @@ -0,0 +1,34 @@ +using Hangfire; +using Meezi.Core.Delivery; +using Meezi.API.Services.Delivery; + +namespace Meezi.API.Jobs; + +public class ProcessDeliveryOrderJob +{ + private readonly IDeliveryOrderProcessor _processor; + private readonly ILogger _logger; + + public ProcessDeliveryOrderJob( + IDeliveryOrderProcessor processor, + ILogger logger) + { + _processor = processor; + _logger = logger; + } + + [AutomaticRetry(Attempts = 3, DelaysInSeconds = [10, 60, 300])] + public async Task ExecuteAsync( + string webhookLogId, + UnifiedDeliveryOrder unified, + CancellationToken cancellationToken) + { + var result = await _processor.ProcessAsync(webhookLogId, unified, cancellationToken); + if (!result.Success && result.ErrorCode is not null) + _logger.LogWarning( + "Delivery process failed {Code}: {Message} (log {LogId})", + result.ErrorCode, + result.Message, + webhookLogId); + } +} diff --git a/src/Meezi.API/Jobs/SubscriptionRenewalReminderJob.cs b/src/Meezi.API/Jobs/SubscriptionRenewalReminderJob.cs new file mode 100644 index 0000000..9928f19 --- /dev/null +++ b/src/Meezi.API/Jobs/SubscriptionRenewalReminderJob.cs @@ -0,0 +1,60 @@ +using Meezi.Core.Enums; +using Meezi.Core.Interfaces; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; + +namespace Meezi.API.Jobs; + +public class SubscriptionRenewalReminderJob +{ + private readonly IServiceScopeFactory _scopeFactory; + private readonly ILogger _logger; + + public SubscriptionRenewalReminderJob( + IServiceScopeFactory scopeFactory, + ILogger logger) + { + _scopeFactory = scopeFactory; + _logger = logger; + } + + public async Task ExecuteAsync() + { + await using var scope = _scopeFactory.CreateAsyncScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var sms = scope.ServiceProvider.GetRequiredService(); + + var windowStart = DateTime.UtcNow.Date; + var windowEnd = windowStart.AddDays(3); + + var cafes = await db.Cafes + .Where(c => c.PlanTier != PlanTier.Free + && c.PlanExpiresAt != null + && c.PlanExpiresAt >= windowStart + && c.PlanExpiresAt <= windowEnd) + .ToListAsync(); + + foreach (var cafe in cafes) + { + var ownerPhone = await db.Employees + .Where(e => e.CafeId == cafe.Id && e.Role == EmployeeRole.Owner) + .Select(e => e.Phone) + .FirstOrDefaultAsync(); + + if (string.IsNullOrEmpty(ownerPhone)) continue; + + var message = + $"میزی: اشتراک {cafe.PlanTier} شما تا {cafe.PlanExpiresAt:yyyy-MM-dd} منقضی می‌شود. از تنظیمات داشبورد تمدید کنید."; + try + { + await sms.SendMessageAsync(ownerPhone, message); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Renewal SMS failed for cafe {CafeId}", cafe.Id); + } + } + + _logger.LogInformation("Subscription renewal reminders sent for {Count} cafes", cafes.Count); + } +} diff --git a/src/Meezi.API/Meezi.API.csproj b/src/Meezi.API/Meezi.API.csproj new file mode 100644 index 0000000..1186f10 --- /dev/null +++ b/src/Meezi.API/Meezi.API.csproj @@ -0,0 +1,41 @@ + + + + Meezi.API + Meezi.API + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Meezi.API/Meezi.API.http b/src/Meezi.API/Meezi.API.http new file mode 100644 index 0000000..502ea53 --- /dev/null +++ b/src/Meezi.API/Meezi.API.http @@ -0,0 +1,6 @@ +@Meezi.API_HostAddress = http://localhost:5011 + +GET {{Meezi.API_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/src/Meezi.API/Middleware/PlanLimitMiddleware.cs b/src/Meezi.API/Middleware/PlanLimitMiddleware.cs new file mode 100644 index 0000000..ca3d66c --- /dev/null +++ b/src/Meezi.API/Middleware/PlanLimitMiddleware.cs @@ -0,0 +1,64 @@ +using System.Text.Json; +using Meezi.API.Services; +using Meezi.Core.Interfaces; +using Meezi.Shared; + +namespace Meezi.API.Middleware; + +public class PlanLimitMiddleware +{ + private static readonly string[] SkipPrefixes = + [ + "/api/auth", + "/api/customers/me", + "/api/admin", + "/hubs/guest-order", + "/api/public", + "/api/q/", + "/api/webhooks", + "/api/billing/verify", + "/health", + "/swagger", + "/hangfire" + ]; + + private readonly RequestDelegate _next; + + public PlanLimitMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task InvokeAsync(HttpContext context, ITenantContext tenant, IPlanLimitChecker planLimitChecker) + { + if (ShouldSkip(context.Request.Path)) + { + await _next(context); + return; + } + + if (context.User.Identity?.IsAuthenticated == true) + { + var (allowed, code, message) = await planLimitChecker.CheckAsync(context, tenant, context.RequestAborted); + if (!allowed) + { + context.Response.StatusCode = StatusCodes.Status403Forbidden; + context.Response.ContentType = "application/json"; + var payload = new ApiResponse(false, null, new ApiError(code!, message!)); + await context.Response.WriteAsync(JsonSerializer.Serialize(payload, new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + })); + return; + } + } + + await _next(context); + } + + private static bool ShouldSkip(PathString path) + { + var value = path.Value ?? string.Empty; + return SkipPrefixes.Any(p => value.StartsWith(p, StringComparison.OrdinalIgnoreCase)); + } +} diff --git a/src/Meezi.API/Middleware/TenantMiddleware.cs b/src/Meezi.API/Middleware/TenantMiddleware.cs new file mode 100644 index 0000000..5dcd636 --- /dev/null +++ b/src/Meezi.API/Middleware/TenantMiddleware.cs @@ -0,0 +1,154 @@ +using System.Text.Json; +using Microsoft.EntityFrameworkCore; +using Meezi.Core.Constants; +using Meezi.Core.Enums; +using Meezi.Core.Interfaces; +using Meezi.Infrastructure.Data; +using Meezi.Shared; + +namespace Meezi.API.Middleware; + +public class TenantMiddleware +{ + private static readonly string[] PublicPrefixes = + [ + "/api/auth", + "/api/public", + "/api/q/", + "/api/webhooks", + "/api/billing/verify", + "/hubs/guest-order", + "/health", + "/swagger", + "/hangfire" + ]; + + private readonly RequestDelegate _next; + private readonly ILogger _logger; + + public TenantMiddleware(RequestDelegate next, ILogger logger) + { + _next = next; + _logger = logger; + } + + public async Task InvokeAsync( + HttpContext context, + ITenantContext tenant, + IBranchContext branchContext, + AppDbContext db) + { + if (IsPublicPath(context.Request.Path)) + { + await _next(context); + return; + } + + if (context.User.Identity?.IsAuthenticated != true) + { + await WriteUnauthorizedAsync(context, "UNAUTHORIZED", "Authentication required."); + return; + } + + var actor = context.User.FindFirst(MeeziClaimTypes.Actor)?.Value; + var pathValue = context.Request.Path.Value ?? string.Empty; + if (actor == MeeziActorKinds.Consumer) + { + if (pathValue.StartsWith("/api/customers/me", StringComparison.OrdinalIgnoreCase)) + { + await _next(context); + return; + } + + await WriteForbiddenAsync(context, "FORBIDDEN", "Consumer access is limited to account endpoints."); + return; + } + + if (tenant is TenantContext scopedTenant) + { + scopedTenant.UserId = context.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value + ?? context.User.FindFirst("sub")?.Value; + scopedTenant.Language = context.User.FindFirst(MeeziClaimTypes.Language)?.Value ?? "fa"; + } + + var cafeId = context.User.FindFirst(MeeziClaimTypes.CafeId)?.Value; + if (string.IsNullOrEmpty(cafeId)) + { + _logger.LogWarning("Authenticated request missing cafeId claim for {Path}", context.Request.Path); + await WriteUnauthorizedAsync(context, "UNAUTHORIZED", "Cafe context is missing."); + return; + } + + var cafeSuspended = await db.Cafes + .AsNoTracking() + .AnyAsync(c => c.Id == cafeId && c.IsSuspended, context.RequestAborted); + if (cafeSuspended) + { + await WriteForbiddenAsync(context, "CAFE_SUSPENDED", "This cafe account is suspended. Contact Meezi support."); + return; + } + + if (tenant is TenantContext scopedMerchant) + { + scopedMerchant.CafeId = cafeId; + + var roleClaim = context.User.FindFirst(MeeziClaimTypes.Role)?.Value; + if (Enum.TryParse(roleClaim, ignoreCase: true, out var role)) + scopedMerchant.Role = role; + + var planClaim = context.User.FindFirst(MeeziClaimTypes.PlanTier)?.Value; + if (Enum.TryParse(planClaim, ignoreCase: true, out var plan)) + scopedMerchant.PlanTier = plan; + + var branchIdClaim = context.User.FindFirst(MeeziClaimTypes.BranchId)?.Value; + if (!string.IsNullOrEmpty(branchIdClaim)) + { + var branchValid = await db.Branches.AnyAsync( + b => b.Id == branchIdClaim && b.CafeId == cafeId && b.IsActive, + context.RequestAborted); + if (branchValid) + scopedMerchant.BranchId = branchIdClaim; + else + _logger.LogWarning("Ignoring invalid or inactive branchId claim for cafe {CafeId}", cafeId); + } + } + + if (branchContext is BranchContext scopedBranch) + { + scopedBranch.CafeId = cafeId; + if (tenant is TenantContext scopedTenantBranch && !string.IsNullOrEmpty(scopedTenantBranch.BranchId)) + scopedBranch.BranchId = scopedTenantBranch.BranchId; + } + + await _next(context); + } + + private static bool IsPublicPath(PathString path) + { + var value = path.Value ?? string.Empty; + return PublicPrefixes.Any(prefix => + value.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)); + } + + private static async Task WriteUnauthorizedAsync(HttpContext context, string code, string message) + { + context.Response.StatusCode = StatusCodes.Status401Unauthorized; + context.Response.ContentType = "application/json"; + var payload = new ApiResponse(false, null, new ApiError(code, message)); + await context.Response.WriteAsync(JsonSerializer.Serialize(payload, new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + })); + } + + private static async Task WriteForbiddenAsync(HttpContext context, string code, string message) + { + context.Response.StatusCode = StatusCodes.Status403Forbidden; + context.Response.ContentType = "application/json"; + var payload = new ApiResponse(false, null, new ApiError(code, message)); + await context.Response.WriteAsync(JsonSerializer.Serialize(payload, new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + })); + } +} diff --git a/src/Meezi.API/Models/Auth/AuthDtos.cs b/src/Meezi.API/Models/Auth/AuthDtos.cs new file mode 100644 index 0000000..b5c8c82 --- /dev/null +++ b/src/Meezi.API/Models/Auth/AuthDtos.cs @@ -0,0 +1,21 @@ +namespace Meezi.API.Models.Auth; + +public record SendOtpRequest(string Phone); + +public record VerifyOtpRequest(string Phone, string Code, string? CafeId = null); + +public record RefreshTokenRequest(string RefreshToken); + +public record AuthTokenResponse( + string AccessToken, + string RefreshToken, + DateTime ExpiresAt, + string UserId, + string CafeId, + string Role, + string PlanTier, + string Language, + string Actor = Meezi.Core.Constants.MeeziActorKinds.Merchant, + string? BranchId = null); + +public record SendOtpResponse(bool Sent, int ExpiresInSeconds); diff --git a/src/Meezi.API/Models/Billing/BillingDtos.cs b/src/Meezi.API/Models/Billing/BillingDtos.cs new file mode 100644 index 0000000..b1ddb28 --- /dev/null +++ b/src/Meezi.API/Models/Billing/BillingDtos.cs @@ -0,0 +1,27 @@ +using Meezi.Core.Enums; + +namespace Meezi.API.Models.Billing; + +public record SubscribeRequest(PlanTier PlanTier, int Months, string? PaymentMethod = null); + +public record PaymentMethodDto(string Id, string DisplayNameFa, bool IsDefault); + +public record SubscribeResponse(string PaymentId, string PaymentUrl); + +public record BillingStatusDto( + PlanTier PlanTier, + DateTime? PlanExpiresAt, + int OrdersToday, + int? OrdersDailyLimit, + int CustomersCount, + int? CustomersLimit, + int SmsUsedThisMonth, + int SmsMonthlyLimit, + bool Menu3dEnabled, + bool MenuAi3dEnabled, + int MenuAi3dUsedThisMonth, + int MenuAi3dMonthlyLimit, + bool DiscoverProfileEnabled, + bool IsPlanExpired); + +public record BillingVerifyResult(bool Success, string RedirectUrl); diff --git a/src/Meezi.API/Models/Branches/BranchDtos.cs b/src/Meezi.API/Models/Branches/BranchDtos.cs new file mode 100644 index 0000000..d6d0389 --- /dev/null +++ b/src/Meezi.API/Models/Branches/BranchDtos.cs @@ -0,0 +1,35 @@ +namespace Meezi.API.Models.Branches; + +public record BranchDto( + string Id, + string Name, + string? Address, + string? City, + string? Phone, + bool IsActive, + string? LoginPhone = null, + string? ManagerName = null, + bool IsPendingDeletion = false, + DateTime? DeletedAt = null, + DateTime? ScheduledPermanentDeleteAt = null, + int? DaysUntilPermanentDelete = null); + +public record CreateBranchRequest( + string Name, + string LoginPhone, + string? ManagerName, + string? Address, + string? City, + string? Phone); + +public record PatchBranchRequest( + string? Name, + string? Address, + string? City, + string? Phone, + bool? IsActive, + string? LogoUrl, + string? WelcomeText, + string? AccentColor, + string? WifiPassword, + decimal? TaxRate); diff --git a/src/Meezi.API/Models/Cafes/CafeSettingsDtos.cs b/src/Meezi.API/Models/Cafes/CafeSettingsDtos.cs new file mode 100644 index 0000000..c7af13b --- /dev/null +++ b/src/Meezi.API/Models/Cafes/CafeSettingsDtos.cs @@ -0,0 +1,31 @@ +namespace Meezi.API.Models.Cafes; + +public record CafeSettingsDto( + string Id, + string Name, + string Slug, + string? Phone, + string? Address, + string? City, + string? Description, + string? LogoUrl, + string? CoverImageUrl, + string? SnappfoodVendorId, + string PlanTier, + DateTime? PlanExpiresAt, + CafeThemeDto Theme, + decimal DefaultTaxRate, + bool AllowBranchTaxOverride); + +public record PatchCafeSettingsRequest( + string? Name, + string? Phone, + string? Address, + string? City, + string? Description, + string? LogoUrl, + string? CoverImageUrl, + string? SnappfoodVendorId, + CafeThemeDto? Theme, + decimal? DefaultTaxRate, + bool? AllowBranchTaxOverride); diff --git a/src/Meezi.API/Models/Cafes/CafeThemeDtos.cs b/src/Meezi.API/Models/Cafes/CafeThemeDtos.cs new file mode 100644 index 0000000..07fad46 --- /dev/null +++ b/src/Meezi.API/Models/Cafes/CafeThemeDtos.cs @@ -0,0 +1,30 @@ +namespace Meezi.API.Models.Cafes; + +public record CafeThemeCustomColorsDto( + string? Primary, + string? Secondary, + string? Accent, + string? Background, + string? Surface, + string? Text, + string? TextMuted, + string? Destructive, + string? Success, + int? PrimaryOpacity = null, + int? SecondaryOpacity = null, + int? AccentOpacity = null, + int? BackgroundOpacity = null, + int? SurfaceOpacity = null, + int? TextOpacity = null, + int? TextMutedOpacity = null, + int? DestructiveOpacity = null, + int? SuccessOpacity = null); + +public record CafeThemeDto( + string PaletteId, + string PanelStyle, + string MenuStyle, + string MenuTexture, + string Density, + string Radius, + CafeThemeCustomColorsDto? Custom); diff --git a/src/Meezi.API/Models/Consumer/ConsumerDtos.cs b/src/Meezi.API/Models/Consumer/ConsumerDtos.cs new file mode 100644 index 0000000..ba8e516 --- /dev/null +++ b/src/Meezi.API/Models/Consumer/ConsumerDtos.cs @@ -0,0 +1,23 @@ +using Meezi.Core.Enums; + +namespace Meezi.API.Models.Consumer; + +public record ConsumerAuthTokenResponse( + string AccessToken, + string RefreshToken, + DateTime ExpiresAt, + string UserId, + string Phone, + string Language); + +public record ConsumerOrderHistoryDto( + string Id, + string CafeId, + string CafeName, + string CafeSlug, + OrderStatus Status, + decimal Total, + int DisplayNumber, + DateTime CreatedAt, + string? TableNumber, + int ItemCount); diff --git a/src/Meezi.API/Models/Crm/CouponDtos.cs b/src/Meezi.API/Models/Crm/CouponDtos.cs new file mode 100644 index 0000000..7bdd4ca --- /dev/null +++ b/src/Meezi.API/Models/Crm/CouponDtos.cs @@ -0,0 +1,50 @@ +using Meezi.Core.Enums; + +namespace Meezi.API.Models.Crm; + +public record CouponDto( + string Id, + string Code, + CouponType Type, + decimal Value, + decimal? MinOrderAmount, + decimal? MaxDiscount, + int? UsageLimit, + int UsedCount, + CustomerGroup? TargetGroup, + DateTime? StartsAt, + DateTime? ExpiresAt, + bool IsActive); + +public record CreateCouponRequest( + string Code, + CouponType Type, + decimal Value, + decimal? MinOrderAmount, + decimal? MaxDiscount, + int? UsageLimit, + CustomerGroup? TargetGroup, + DateTime? StartsAt, + DateTime? ExpiresAt, + bool IsActive = true); + +public record ValidateCouponRequest(string Code, decimal Subtotal); + +public record ValidateCouponResult( + string CouponId, + string Code, + CouponType Type, + decimal Value, + decimal DiscountAmount); + +public record UpdateCouponRequest( + string? Code, + CouponType? Type, + decimal? Value, + decimal? MinOrderAmount, + decimal? MaxDiscount, + int? UsageLimit, + CustomerGroup? TargetGroup, + DateTime? StartsAt, + DateTime? ExpiresAt, + bool? IsActive); diff --git a/src/Meezi.API/Models/Crm/CustomerDtos.cs b/src/Meezi.API/Models/Crm/CustomerDtos.cs new file mode 100644 index 0000000..d6548e9 --- /dev/null +++ b/src/Meezi.API/Models/Crm/CustomerDtos.cs @@ -0,0 +1,31 @@ +using Meezi.Core.Enums; + +namespace Meezi.API.Models.Crm; + +public record CustomerDto( + string Id, + string Name, + string Phone, + string? NationalId, + string? BirthDateJalali, + CustomerGroup Group, + int LoyaltyPoints, + string? ReferredBy, + DateTime CreatedAt); + +public record CreateCustomerRequest( + string Name, + string Phone, + string? NationalId, + string? BirthDateJalali, + CustomerGroup Group = CustomerGroup.Regular, + string? ReferredBy = null); + +public record UpdateCustomerRequest( + string? Name, + string? Phone, + string? NationalId, + string? BirthDateJalali, + CustomerGroup? Group, + int? LoyaltyPoints, + string? ReferredBy); diff --git a/src/Meezi.API/Models/Crm/SmsDtos.cs b/src/Meezi.API/Models/Crm/SmsDtos.cs new file mode 100644 index 0000000..e7f8caa --- /dev/null +++ b/src/Meezi.API/Models/Crm/SmsDtos.cs @@ -0,0 +1,12 @@ +using Meezi.Core.Enums; + +namespace Meezi.API.Models.Crm; + +public record SendSmsCampaignRequest( + string Message, + CustomerGroup? TargetGroup, + IReadOnlyList? Phones); + +public record SmsCampaignResult(int SentCount, int FailedCount); + +public record SmsUsageDto(int UsedThisMonth, int MonthlyLimit, string Month); diff --git a/src/Meezi.API/Models/Delivery/DeliveryDtos.cs b/src/Meezi.API/Models/Delivery/DeliveryDtos.cs new file mode 100644 index 0000000..1e52339 --- /dev/null +++ b/src/Meezi.API/Models/Delivery/DeliveryDtos.cs @@ -0,0 +1,20 @@ +using Meezi.Core.Enums; + +namespace Meezi.API.Models.Delivery; + +public record PlatformRevenueDto( + DeliveryPlatform Platform, + string PlatformName, + int OrderCount, + decimal GrossRevenue, + decimal Commission, + decimal NetRevenue); + +public record DeliveryRevenueReportDto( + string PeriodLabel, + DateTime UtcFrom, + DateTime UtcTo, + IReadOnlyList Platforms, + decimal TotalGross, + decimal TotalCommission, + decimal TotalNet); diff --git a/src/Meezi.API/Models/Discover/CafeDiscoverProfileDtos.cs b/src/Meezi.API/Models/Discover/CafeDiscoverProfileDtos.cs new file mode 100644 index 0000000..5d56a92 --- /dev/null +++ b/src/Meezi.API/Models/Discover/CafeDiscoverProfileDtos.cs @@ -0,0 +1,31 @@ +namespace Meezi.API.Models.Discover; + +public record CafeDiscoverProfileDto( + IReadOnlyList Themes, + string? Size, + string? Floors, + IReadOnlyList Vibes, + IReadOnlyList Occasions, + IReadOnlyList SpaceFeatures, + string? NoiseLevel, + string? PriceTier); + +public record UpsertCafeDiscoverProfileRequest( + IReadOnlyList? Themes, + string? Size, + string? Floors, + IReadOnlyList? Vibes, + IReadOnlyList? Occasions, + IReadOnlyList? SpaceFeatures, + string? NoiseLevel, + string? PriceTier); + +public record DiscoverProfileTaxonomyDto( + IReadOnlyList Themes, + IReadOnlyList Sizes, + IReadOnlyList Floors, + IReadOnlyList Vibes, + IReadOnlyList Occasions, + IReadOnlyList SpaceFeatures, + IReadOnlyList NoiseLevels, + IReadOnlyList PriceTiers); diff --git a/src/Meezi.API/Models/Expenses/ExpenseDtos.cs b/src/Meezi.API/Models/Expenses/ExpenseDtos.cs new file mode 100644 index 0000000..24ef143 --- /dev/null +++ b/src/Meezi.API/Models/Expenses/ExpenseDtos.cs @@ -0,0 +1,25 @@ +using Meezi.Core.Enums; + +namespace Meezi.API.Models.Expenses; + +public record CreateExpenseRequest( + string BranchId, + string? ShiftId, + ExpenseCategory Category, + decimal Amount, + string? Note, + string? ReceiptImageUrl); + +public record ExpenseDto( + string Id, + string CafeId, + string BranchId, + string? ShiftId, + ExpenseCategory Category, + decimal Amount, + string? Note, + string? ReceiptImageUrl, + string CreatedByUserId, + DateTime CreatedAt); + +public record ExpenseListResult(IReadOnlyList Items, int Total); diff --git a/src/Meezi.API/Models/Hr/HrDtos.cs b/src/Meezi.API/Models/Hr/HrDtos.cs new file mode 100644 index 0000000..690e2e6 --- /dev/null +++ b/src/Meezi.API/Models/Hr/HrDtos.cs @@ -0,0 +1,61 @@ +using Meezi.Core.Enums; + +namespace Meezi.API.Models.Hr; + +public record EmployeeSummaryDto( + string Id, + string Name, + string Phone, + EmployeeRole Role, + decimal BaseSalary); + +public record AttendanceDto( + string Id, + string EmployeeId, + string EmployeeName, + DateOnly Date, + DateTime? ClockIn, + DateTime? ClockOut, + string? Notes); + +public record ShiftDto(int DayOfWeek, ShiftType ShiftType); + +public record UpsertShiftsRequest(IReadOnlyList Shifts); + +public record LeaveRequestDto( + string Id, + string EmployeeId, + string EmployeeName, + DateOnly StartDate, + DateOnly EndDate, + string? Reason, + LeaveStatus Status, + string? ReviewedBy, + DateTime CreatedAt); + +public record CreateLeaveRequest( + DateOnly StartDate, + DateOnly EndDate, + string? Reason); + +public record ReviewLeaveRequest(LeaveStatus Status); + +public record EmployeeSalaryDto( + string Id, + string EmployeeId, + string EmployeeName, + string MonthYear, + decimal BaseSalary, + decimal OvertimePay, + decimal Deductions, + decimal NetSalary, + bool IsPaid); + +public record CreateSalaryRequest( + string EmployeeId, + string MonthYear, + decimal BaseSalary, + decimal OvertimePay, + decimal Deductions); + +public record TodayShiftDto(ShiftType ShiftType, string Label); diff --git a/src/Meezi.API/Models/Kitchen/KitchenStationDtos.cs b/src/Meezi.API/Models/Kitchen/KitchenStationDtos.cs new file mode 100644 index 0000000..603e426 --- /dev/null +++ b/src/Meezi.API/Models/Kitchen/KitchenStationDtos.cs @@ -0,0 +1,24 @@ +namespace Meezi.API.Models.Kitchen; + +public record KitchenStationDto( + string Id, + string? BranchId, + string Name, + string? PrinterIp, + int PrinterPort, + int SortOrder, + int CategoryCount); + +public record CreateKitchenStationRequest( + string Name, + string? BranchId, + string? PrinterIp, + int PrinterPort = 9100, + int SortOrder = 0); + +public record UpdateKitchenStationRequest( + string? Name, + string? BranchId, + string? PrinterIp, + int? PrinterPort, + int? SortOrder); diff --git a/src/Meezi.API/Models/Menu/BranchMenuDtos.cs b/src/Meezi.API/Models/Menu/BranchMenuDtos.cs new file mode 100644 index 0000000..c4f8b4e --- /dev/null +++ b/src/Meezi.API/Models/Menu/BranchMenuDtos.cs @@ -0,0 +1,29 @@ +namespace Meezi.API.Models.Menu; + +public record BranchMenuItemDto( + string Id, + string CategoryId, + string Name, + string? NameAr, + string? NameEn, + string? Description, + decimal MasterPrice, + decimal EffectivePrice, + decimal DiscountPercent, + string? ImageUrl, + string? VideoUrl, + string? Model3dUrl, + bool IsAvailable, + bool IsOverridden, + bool HasPriceOverride); + +public record UpsertBranchMenuOverrideRequest( + bool IsAvailable, + decimal? PriceOverride); + +public record BranchMenuOverrideDto( + string MenuItemId, + string BranchId, + bool IsAvailable, + decimal? PriceOverride, + DateTime UpdatedAt); diff --git a/src/Meezi.API/Models/Menu/MenuDtos.cs b/src/Meezi.API/Models/Menu/MenuDtos.cs new file mode 100644 index 0000000..2c222f6 --- /dev/null +++ b/src/Meezi.API/Models/Menu/MenuDtos.cs @@ -0,0 +1,86 @@ +namespace Meezi.API.Models.Menu; + +public record MenuCategoryDto( + string Id, + string Name, + string? NameAr, + string? NameEn, + int SortOrder, + string? TaxId, + decimal DiscountPercent, + string? Icon, + string? IconPresetId, + string? IconStyle, + string? ImageUrl, + bool IsActive, + string? KitchenStationId); + +public record CreateMenuCategoryRequest( + string Name, + string? NameAr, + string? NameEn, + int SortOrder, + string? TaxId, + decimal DiscountPercent, + string? Icon = null, + string? IconPresetId = null, + string? IconStyle = null, + string? ImageUrl = null, + bool IsActive = true, + string? KitchenStationId = null); + +public record UpdateMenuCategoryRequest( + string? Name, + string? NameAr, + string? NameEn, + int? SortOrder, + string? TaxId, + decimal? DiscountPercent, + string? Icon, + string? IconPresetId, + string? IconStyle, + string? ImageUrl, + bool? IsActive, + string? KitchenStationId); + +public record MenuItemDto( + string Id, + string CategoryId, + string Name, + string? NameAr, + string? NameEn, + string? Description, + decimal Price, + decimal DiscountPercent, + string? ImageUrl, + string? VideoUrl, + string? Model3dUrl, + bool IsAvailable); + +public record CreateMenuItemRequest( + string CategoryId, + string Name, + string? NameAr, + string? NameEn, + string? Description, + decimal Price, + decimal DiscountPercent = 0, + string? ImageUrl = null, + string? VideoUrl = null, + string? Model3dUrl = null, + bool IsAvailable = true); + +public record UpdateMenuItemRequest( + string? CategoryId, + string? Name, + string? NameAr, + string? NameEn, + string? Description, + decimal? Price, + decimal? DiscountPercent, + string? ImageUrl, + string? VideoUrl, + string? Model3dUrl, + bool? IsAvailable); + +public record UpdateMenuItemAvailabilityRequest(bool IsAvailable); diff --git a/src/Meezi.API/Models/Notifications/NotificationDtos.cs b/src/Meezi.API/Models/Notifications/NotificationDtos.cs new file mode 100644 index 0000000..a198e01 --- /dev/null +++ b/src/Meezi.API/Models/Notifications/NotificationDtos.cs @@ -0,0 +1,19 @@ +namespace Meezi.API.Models.Notifications; + +public record CafeNotificationDto( + string Id, + string Type, + string Title, + string? Body, + string? ReferenceId, + string? TableNumber, + bool IsRead, + DateTime CreatedAt); + +public record NotificationListDto( + IReadOnlyList Items, + int UnreadCount); + +public record MarkNotificationsReadRequest( + IReadOnlyList? Ids, + bool All = false); diff --git a/src/Meezi.API/Models/Orders/OrderDtos.cs b/src/Meezi.API/Models/Orders/OrderDtos.cs new file mode 100644 index 0000000..be33a88 --- /dev/null +++ b/src/Meezi.API/Models/Orders/OrderDtos.cs @@ -0,0 +1,81 @@ +using Meezi.Core.Enums; + +namespace Meezi.API.Models.Orders; + +public record OrderItemDto( + string Id, + string MenuItemId, + string MenuItemName, + int Quantity, + decimal UnitPrice, + string? Notes, + bool IsVoided = false, + DateTime? VoidedAt = null); + +public record TransferTableRequest(string TargetTableId); + +public record OrderDto( + string Id, + string CafeId, + string? BranchId, + string? TableId, + string? TableNumber, + string? GuestName, + string? GuestPhone, + string? CustomerName, + string? CustomerPhone, + string? CustomerId, + string? EmployeeId, + OrderType OrderType, + OrderSource Source, + OrderStatus Status, + decimal Subtotal, + decimal TaxTotal, + decimal DiscountAmount, + decimal Total, + decimal PaidAmount, + DateTime CreatedAt, + int DisplayNumber, + IReadOnlyList Items, + IReadOnlyList Payments); + +public record AppendOrderItemsRequest(IReadOnlyList Items); + +public record UpdateOrderSessionRequest( + string? GuestName, + string? GuestPhone, + string? CustomerId); + + +public record CreateOrderItemRequest(string MenuItemId, int Quantity, string? Notes); + +public record CreateOrderRequest( + OrderType OrderType, + string? BranchId, + string? TableId, + string? ReservationId, + string? GuestName, + string? GuestPhone, + string? CustomerId, + string? CouponId, + IReadOnlyList Items); + +public record UpdateOrderStatusRequest(OrderStatus Status); + +public record CreatePaymentRequest(PaymentMethod Method, decimal Amount, string? Reference); + +public record RecordPaymentsRequest( + IReadOnlyList Payments, + int? LoyaltyPointsToRedeem = null); + +public record PaymentDto(string Id, PaymentMethod Method, decimal Amount, PaymentStatus Status, string? Reference); + +public record LiveOrderDto( + string Id, + int DisplayNumber, + OrderStatus Status, + string? TableNumber, + OrderType OrderType, + decimal Total, + DateTime CreatedAt, + IReadOnlyList Items); diff --git a/src/Meezi.API/Models/Printing/PrintDtos.cs b/src/Meezi.API/Models/Printing/PrintDtos.cs new file mode 100644 index 0000000..6d52d64 --- /dev/null +++ b/src/Meezi.API/Models/Printing/PrintDtos.cs @@ -0,0 +1,36 @@ +namespace Meezi.API.Models.Printing; + +public record BranchPrintSettingsDto( + string BranchId, + string? ReceiptPrinterIp, + int? ReceiptPrinterPort, + string? KitchenPrinterIp, + int? KitchenPrinterPort, + int PaperWidthMm, + bool AutoCutEnabled, + string? ReceiptHeader, + string? ReceiptFooter, + string? WifiPassword, + string? PosDeviceIp, + int? PosDevicePort); + +public record PatchBranchPrintSettingsRequest( + string? ReceiptPrinterIp, + int? ReceiptPrinterPort, + string? KitchenPrinterIp, + int? KitchenPrinterPort, + int? PaperWidthMm, + bool? AutoCutEnabled, + string? ReceiptHeader, + string? ReceiptFooter, + string? WifiPassword, + string? PosDeviceIp, + int? PosDevicePort); + +public record PosPaymentRequest(string OrderId, decimal Amount); + +public record PosPaymentResultDto(bool Sent, bool Skipped, string? Message); + +public record TestPrintRequest(string PrinterIp, int Port = 9100); + +public record PrintJobResultDto(bool Printed, string? ErrorCode, string? Message); diff --git a/src/Meezi.API/Models/Public/CoffeeAdvisorDtos.cs b/src/Meezi.API/Models/Public/CoffeeAdvisorDtos.cs new file mode 100644 index 0000000..1ae2a5a --- /dev/null +++ b/src/Meezi.API/Models/Public/CoffeeAdvisorDtos.cs @@ -0,0 +1,9 @@ +namespace Meezi.API.Models.Public; + +public record CoffeeAdvisorRequest(string Purpose, string? CafeSlug = null); + +public record CoffeeAdvisorPickDto(string Name, string Reason, string? MenuItemId); + +public record CoffeeAdvisorResultDto( + string Summary, + IReadOnlyList Picks); diff --git a/src/Meezi.API/Models/Public/PublicDtos.cs b/src/Meezi.API/Models/Public/PublicDtos.cs new file mode 100644 index 0000000..9293acb --- /dev/null +++ b/src/Meezi.API/Models/Public/PublicDtos.cs @@ -0,0 +1,187 @@ +using Meezi.API.Models.Cafes; +using Meezi.API.Models.Discover; +using Meezi.API.Models.Orders; +using Meezi.Core.Enums; + +namespace Meezi.API.Models.Public; + +// ── Working hours (read-only, for public display) ──────────────────────────── + +public record DaySchedulePublicDto(bool IsOpen, string? Open, string? Close); + +public record WorkingHoursPublicDto( + DaySchedulePublicDto? Sat, + DaySchedulePublicDto? Sun, + DaySchedulePublicDto? Mon, + DaySchedulePublicDto? Tue, + DaySchedulePublicDto? Wed, + DaySchedulePublicDto? Thu, + DaySchedulePublicDto? Fri); + +// ── NLP parse result (returned by /api/public/discover/nlp-parse) ──────────── + +public record DiscoverNlpHintsDto( + IReadOnlyList Themes, + IReadOnlyList Vibes, + IReadOnlyList Occasions, + IReadOnlyList SpaceFeatures, + string? NoiseLevel, + string? PriceTier, + string? Size) +{ + public static readonly DiscoverNlpHintsDto Empty = + new([], [], [], [], null, null, null); +} + +public record CafeBadgePublicDto(string Key, string Label, string Icon); + +public record CafeDiscoverDto( + string Id, + string Name, + string Slug, + string? City, + string? Address, + string? LogoUrl, + string? CoverImageUrl, + bool IsVerified, + double AverageRating, + int ReviewCount, + CafeDiscoverProfileDto DiscoverProfile, + IReadOnlyList Badges, + IReadOnlyList GalleryUrls, + bool IsOpenNow, + string? InstagramHandle, + string? WebsiteUrl, + double RelevanceScore); + +public record CafePublicDto( + string Id, + string Name, + string? NameAr, + string? NameEn, + string Slug, + string? City, + string? Address, + string? Phone, + string? LogoUrl, + string? CoverImageUrl, + string? Description, + bool IsVerified, + double AverageRating, + int ReviewCount, + CafeDiscoverProfileDto DiscoverProfile, + IReadOnlyList Badges, + IReadOnlyList GalleryUrls, + bool IsOpenNow, + string? InstagramHandle, + string? WebsiteUrl, + WorkingHoursPublicDto? WorkingHours); + +public record PublicMenuItemDto( + string Id, + string CategoryId, + string Name, + string? NameAr, + string? NameEn, + string? Description, + decimal Price, + decimal DiscountPercent, + string? ImageUrl, + string? VideoUrl, + string? Model3dUrl, + bool IsAvailable); + +public record PublicMenuCategoryDto( + string Id, + string Name, + string? NameAr, + string? NameEn, + string? Icon, + string? IconPresetId, + string? IconStyle, + string? ImageUrl, + IReadOnlyList Items); + +public record PublicMenuDto( + string CafeId, + string CafeName, + string Slug, + CafeThemeDto Theme, + IReadOnlyList Categories); + +public record GuestCreateOrderRequest( + OrderType OrderType, + string? TableId, + string? GuestPhone, + string? GuestName, + string? CouponCode, + IReadOnlyList Items, + string? CaptchaToken = null); + +public record GuestOrderPlacedDto( + string OrderId, + OrderStatus Status, + decimal Total, + string? TableNumber); + +public record PlaceGuestOrderRequest( + string TableId, + string? GuestName, + string? GuestPhone, + IReadOnlyList Items, + string? CaptchaToken = null); + +public record PublicSecurityConfigDto( + bool AbuseProtectionEnabled, + string? TurnstileSiteKey, + bool CaptchaRequired); + +public record GuestQrOrderPlacedDto( + string OrderId, + string OrderNumber, + decimal TotalAmount, + int ItemCount, + OrderStatus Status, + string TrackingToken); + +public record OrderTrackingStepDto( + string Key, + string LabelKey, + bool IsComplete, + bool IsCurrent); + +public record OrderTrackDto( + string Id, + string OrderNumber, + OrderStatus Status, + string StatusLabelKey, + decimal Total, + string? TableNumber, + DateTime CreatedAt, + DateTime StatusUpdatedAt, + string TrackingToken, + IReadOnlyList Steps, + IReadOnlyList Items); + +public record CreateReservationRequest( + string GuestName, + string GuestPhone, + DateOnly Date, + TimeOnly Time, + int PartySize, + string? Notes, + string? TableId, + string? CaptchaToken = null); + +public record ReservationDto( + string Id, + string CafeId, + string? TableId, + string? TableNumber, + string GuestName, + string GuestPhone, + DateOnly Date, + TimeOnly Time, + int PartySize, + ReservationStatus Status, + string? Notes); diff --git a/src/Meezi.API/Models/Public/ReviewDtos.cs b/src/Meezi.API/Models/Public/ReviewDtos.cs new file mode 100644 index 0000000..38a6812 --- /dev/null +++ b/src/Meezi.API/Models/Public/ReviewDtos.cs @@ -0,0 +1,22 @@ +namespace Meezi.API.Models.Public; + +public record CafeReviewDto( + string Id, + string AuthorName, + int Rating, + string? Comment, + string? OwnerReply, + DateTime CreatedAt, + IReadOnlyList PhotoUrls, + bool IsHidden = false); + +public record CreateCafeReviewRequest( + string AuthorName, + string? AuthorPhone, + int Rating, + string? Comment, + string? CaptchaToken = null); + +public record ReplyCafeReviewRequest(string Reply); + +public record HideCafeReviewRequest(bool IsHidden); diff --git a/src/Meezi.API/Models/Queue/QueueDtos.cs b/src/Meezi.API/Models/Queue/QueueDtos.cs new file mode 100644 index 0000000..2ed9c8e --- /dev/null +++ b/src/Meezi.API/Models/Queue/QueueDtos.cs @@ -0,0 +1,27 @@ +using Meezi.Core.Enums; + +namespace Meezi.API.Models.Queue; + +public record QueueTicketDto( + string Id, + string? BranchId, + DateOnly ServiceDate, + int Number, + string? CustomerLabel, + string? OrderId, + QueueTicketStatus Status, + DateTime IssuedAt); + +public record QueueBoardDto( + DateOnly ServiceDate, + int? NowServing, + int LastIssued, + int WaitingCount, + IReadOnlyList Tickets); + +public record IssueQueueTicketRequest( + string? BranchId, + string? CustomerLabel, + string? OrderId); + +public record UpdateQueueTicketStatusRequest(QueueTicketStatus Status); diff --git a/src/Meezi.API/Models/Reports/DailyReportSnapshotDtos.cs b/src/Meezi.API/Models/Reports/DailyReportSnapshotDtos.cs new file mode 100644 index 0000000..6b3a4f7 --- /dev/null +++ b/src/Meezi.API/Models/Reports/DailyReportSnapshotDtos.cs @@ -0,0 +1,35 @@ +namespace Meezi.API.Models.Reports; + +public record TopProductSnapshotDto(string ProductId, string Name, int Quantity, decimal Revenue); + +public record DailyReportSnapshotDto( + string Id, + string CafeId, + string BranchId, + string Date, + decimal TotalRevenue, + decimal CashRevenue, + decimal CardRevenue, + decimal CreditRevenue, + int TotalOrders, + decimal AvgOrderValue, + int TotalVoids, + decimal VoidAmount, + decimal TotalExpenses, + decimal NetIncome, + IReadOnlyList TopProducts, + DateTime GeneratedAt); + +public record DailyReportSummaryDto( + int Days, + decimal TotalRevenue, + decimal CashRevenue, + decimal CardRevenue, + decimal CreditRevenue, + int TotalOrders, + decimal AvgOrderValue, + int TotalVoids, + decimal VoidAmount, + decimal TotalExpenses, + decimal NetIncome, + IReadOnlyList ByBranch); diff --git a/src/Meezi.API/Models/Reports/ReportDtos.cs b/src/Meezi.API/Models/Reports/ReportDtos.cs new file mode 100644 index 0000000..68cbc83 --- /dev/null +++ b/src/Meezi.API/Models/Reports/ReportDtos.cs @@ -0,0 +1,26 @@ +namespace Meezi.API.Models.Reports; + +public record TopItemDto(string MenuItemId, string Name, int Quantity, decimal Revenue); + +public record DailyReportDto( + string DateJalali, + int TotalOrders, + int NewCustomers, + int ReturningCustomers, + decimal Revenue, + decimal TaxTotal, + decimal DiscountTotal, + IReadOnlyList TopItems); + +public record DailyBreakdownDto(string DateJalali, decimal Revenue, decimal Cost); + +public record MonthlyReportDto( + string MonthJalali, + IReadOnlyList DailyBreakdown, + decimal TotalRevenue, + decimal TotalCosts, + decimal SalaryCosts, + decimal OtherCosts, + decimal NetProfit); + +public record TrendDayDto(string DateJalali, decimal Revenue, decimal Cost); diff --git a/src/Meezi.API/Models/Shifts/ShiftDtos.cs b/src/Meezi.API/Models/Shifts/ShiftDtos.cs new file mode 100644 index 0000000..a7a3201 --- /dev/null +++ b/src/Meezi.API/Models/Shifts/ShiftDtos.cs @@ -0,0 +1,33 @@ +using Meezi.Core.Enums; + +namespace Meezi.API.Models.Shifts; + +public record ShiftDto( + string Id, + string CafeId, + string BranchId, + string OpenedByUserId, + string? ClosedByUserId, + DateTime OpenedAt, + DateTime? ClosedAt, + decimal OpeningCash, + decimal? ClosingCash, + decimal ExpectedCash, + decimal? Discrepancy, + ShiftStatus Status); + +public record CashTransactionDto( + string Id, + string ShiftId, + string? BranchId, + CashTransactionType Type, + PaymentMethod Method, + decimal Amount, + string? ReferenceId, + string? Note, + string CreatedByUserId, + DateTime CreatedAt); + +public record OpenShiftRequest(decimal OpeningCash); + +public record CloseShiftRequest(decimal ClosingCash); diff --git a/src/Meezi.API/Models/Snappfood/SnappfoodDtos.cs b/src/Meezi.API/Models/Snappfood/SnappfoodDtos.cs new file mode 100644 index 0000000..28f0721 --- /dev/null +++ b/src/Meezi.API/Models/Snappfood/SnappfoodDtos.cs @@ -0,0 +1,14 @@ +namespace Meezi.API.Models.Snappfood; + +public record SnappfoodWebhookOrder( + string OrderId, + string VendorId, + string? CustomerName, + string? CustomerPhone, + decimal Total, + IReadOnlyList Items); + +public record SnappfoodWebhookItem( + string Name, + int Quantity, + decimal UnitPrice); diff --git a/src/Meezi.API/Models/Tables/TableDtos.cs b/src/Meezi.API/Models/Tables/TableDtos.cs new file mode 100644 index 0000000..215a350 --- /dev/null +++ b/src/Meezi.API/Models/Tables/TableDtos.cs @@ -0,0 +1,210 @@ +using Meezi.Core.Enums; + + + +namespace Meezi.API.Models.Tables; + + + +public record TableDto( + + string Id, + + string BranchId, + + string? SectionId, + + string? SectionName, + + int SortOrder, + + string Number, + + int Capacity, + + string? Floor, + + string QrCode, + + string QrCodeUrl, + + string? ImageUrl, + + string? VideoUrl, + + bool IsActive); + + + +public record CreateTableRequest( + + string? BranchId, + + string Number, + + int Capacity = 4, + + string? Floor = null, + + bool IsActive = true); + + + +public record CreateBranchTableRequest( + + string Number, + + int Capacity = 4, + + string? Floor = null, + + string? SectionId = null, + + int SortOrder = 0, + + bool IsActive = true); + + + +public record PatchTableRequest( + + string? Number, + + int? Capacity, + + string? Floor, + + string? BranchId, + + string? ImageUrl, + + string? VideoUrl, + + bool? IsActive, + + bool? IsCleaning); + + + +public record PatchBranchTableRequest( + + string? Number, + + int? Capacity, + + string? Floor, + + string? SectionId, + + int? SortOrder, + + string? ImageUrl, + + string? VideoUrl, + + bool? IsActive, + + bool? IsCleaning); + + + +public record TableSectionDto( + + string Id, + + string BranchId, + + string Name, + + int SortOrder, + + bool IsActive, + + int TableCount); + + + +public record CreateTableSectionRequest(string Name, int SortOrder = 0); + + + +public record PatchTableSectionRequest( + + string? Name, + + int? SortOrder, + + bool? IsActive); + + + +public record SetTableCleaningRequest(bool IsCleaning); + + + +public record QrResolveResponse( + string CafeId, + string CafeSlug, + string TableId, + string TableNumber, + string TableName, + string BranchId, + string BranchName, + string CafeName, + string PrimaryColor, + string? LogoUrl, + string WelcomeText, + string? WifiPassword, + string? Address, + bool IsCleaning); + + + +public record TableCurrentOrderSummary( + string OrderId, + OrderStatus Status, + decimal Total, + string? GuestLabel, + OrderSource? Source = null); + + + +public record TableBoardDto( + + string Id, + + string BranchId, + + string? SectionId, + + string? SectionName, + + int SortOrder, + + string Number, + + int Capacity, + + string? Floor, + + string QrCode, + + string QrCodeUrl, + + string? ImageUrl, + + string? VideoUrl, + + bool IsActive, + + TableBoardStatus Status, + + TableCurrentOrderSummary? CurrentOrder, + + bool IsCleaning); + + + +public record BranchTableOperationResult(bool Success, T? Data, string? ErrorCode, string? Message); + + diff --git a/src/Meezi.API/Models/Tap30/Tap30Dtos.cs b/src/Meezi.API/Models/Tap30/Tap30Dtos.cs new file mode 100644 index 0000000..d086737 --- /dev/null +++ b/src/Meezi.API/Models/Tap30/Tap30Dtos.cs @@ -0,0 +1,30 @@ +namespace Meezi.API.Models.Tap30; + +public record Tap30WebhookOrder( + string OrderId, + string VendorId, + Tap30Customer? Customer, + decimal Total, + string? PaymentMethod, + bool? IsPaid, + decimal? Commission, + string? DeliveryType, + int? EstimatedMinutes, + string? DriverName, + string? DriverPhone, + string? Status, + IReadOnlyList? Items); + +public record Tap30Customer( + string? Name, + string? Phone, + string? Address, + double? Lat, + double? Lng); + +public record Tap30WebhookItem( + string? Sku, + string Name, + int Quantity, + decimal UnitPrice, + string? Notes); diff --git a/src/Meezi.API/Models/Taxes/TaxDtos.cs b/src/Meezi.API/Models/Taxes/TaxDtos.cs new file mode 100644 index 0000000..69ac631 --- /dev/null +++ b/src/Meezi.API/Models/Taxes/TaxDtos.cs @@ -0,0 +1,23 @@ +namespace Meezi.API.Models.Taxes; + +public record TaxDto( + string Id, + string Name, + decimal Rate, + bool IsDefault, + bool IsRequired, + bool IsCompound); + +public record CreateTaxRequest( + string Name, + decimal Rate, + bool IsDefault = false, + bool IsRequired = true, + bool IsCompound = false); + +public record UpdateTaxRequest( + string? Name, + decimal? Rate, + bool? IsDefault, + bool? IsRequired, + bool? IsCompound); diff --git a/src/Meezi.API/Program.cs b/src/Meezi.API/Program.cs new file mode 100644 index 0000000..512cf74 --- /dev/null +++ b/src/Meezi.API/Program.cs @@ -0,0 +1,77 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Hosting; +using Meezi.API.Extensions; +using Meezi.Infrastructure.Data; +using Serilog; + +namespace Meezi.API; + +public partial class Program +{ + public static async Task Main(string[] args) + { + Log.Logger = new LoggerConfiguration() + .WriteTo.Console() + .CreateLogger(); + + var hostAborted = false; + try + { + var app = BuildWebApplication(args); + + if (app.Configuration.GetValue("RUN_MIGRATIONS")) + { + await using var scope = app.Services.CreateAsyncScope(); + var db = scope.ServiceProvider.GetRequiredService(); + await db.Database.MigrateAsync(); + await DatabaseSchemaPatches.ApplyAsync(db); + } + + await PlatformDataSeeder.EnsureCatalogUpgradesAsync(app.Services); + + if (!app.Configuration.GetValue("Testing:SkipSeed")) + { + await DevelopmentDataSeeder.SeedAsync(app.Services); + await PlatformDataSeeder.SeedAsync(app.Services); + } + + await app.RunAsync(); + } + catch (HostAbortedException) + { + hostAborted = true; + throw; + } + catch (Exception ex) + { + Log.Fatal(ex, "Application terminated unexpectedly"); + } + finally + { + if (!hostAborted) + await Log.CloseAndFlushAsync(); + } + } + + public static WebApplication BuildWebApplication( + string[] args, + Action? configureBeforeServices = null, + Action? configureAfterServices = null) + { + var builder = WebApplication.CreateBuilder(args); + configureBeforeServices?.Invoke(builder); + + builder.Host.UseSerilog((context, services, configuration) => configuration + .ReadFrom.Configuration(context.Configuration) + .ReadFrom.Services(services) + .Enrich.FromLogContext() + .WriteTo.Console()); + + builder.Services.AddMeeziServices(builder.Configuration); + configureAfterServices?.Invoke(builder); + + var app = builder.Build(); + app.ConfigureMeeziPipeline(); + return app; + } +} diff --git a/src/Meezi.API/Properties/launchSettings.json b/src/Meezi.API/Properties/launchSettings.json new file mode 100644 index 0000000..141fe56 --- /dev/null +++ b/src/Meezi.API/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:18189", + "sslPort": 44310 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5011", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7208;http://localhost:5011", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/Meezi.API/Security/AbuseProtectionOptions.cs b/src/Meezi.API/Security/AbuseProtectionOptions.cs new file mode 100644 index 0000000..817db27 --- /dev/null +++ b/src/Meezi.API/Security/AbuseProtectionOptions.cs @@ -0,0 +1,37 @@ +namespace Meezi.API.Security; + +public class AbuseProtectionOptions +{ + public const string SectionName = "Security"; + + public bool Enabled { get; set; } = true; + + /// When true, public write endpoints require a valid Turnstile token if SecretKey is set. + public bool RequireCaptchaOnPublicWrites { get; set; } + + public TurnstileOptions Turnstile { get; set; } = new(); + + public RateLimitOptions RateLimits { get; set; } = new(); + + public GuestOrderLimits GuestOrders { get; set; } = new(); +} + +public class TurnstileOptions +{ + public string SiteKey { get; set; } = ""; + public string SecretKey { get; set; } = ""; +} + +public class RateLimitOptions +{ + public int AuthOtpPerIpPerHour { get; set; } = 15; + public int PublicReadsPerIpPerMinute { get; set; } = 120; + public int PublicWritesPerIpPerMinute { get; set; } = 30; +} + +public class GuestOrderLimits +{ + public int PerIpPerCafePerHour { get; set; } = 25; + public int PerCafePerHour { get; set; } = 200; + public int PerIpGlobalPerHour { get; set; } = 60; +} diff --git a/src/Meezi.API/Security/AbuseProtectionService.cs b/src/Meezi.API/Security/AbuseProtectionService.cs new file mode 100644 index 0000000..d418e96 --- /dev/null +++ b/src/Meezi.API/Security/AbuseProtectionService.cs @@ -0,0 +1,142 @@ +using System.Net.Http.Json; +using System.Text.Json.Serialization; +using Microsoft.Extensions.Options; +using StackExchange.Redis; + +namespace Meezi.API.Security; + +public class AbuseProtectionService : IAbuseProtectionService +{ + private readonly IConnectionMultiplexer _redis; + private readonly IHttpClientFactory _httpClientFactory; + private readonly AbuseProtectionOptions _options; + private readonly ILogger _logger; + + public AbuseProtectionService( + IConnectionMultiplexer redis, + IHttpClientFactory httpClientFactory, + IOptions options, + ILogger logger) + { + _redis = redis; + _httpClientFactory = httpClientFactory; + _options = options.Value; + _logger = logger; + } + + public bool IsCaptchaConfigured => + !string.IsNullOrWhiteSpace(_options.Turnstile.SecretKey); + + public string? CaptchaSiteKey => + string.IsNullOrWhiteSpace(_options.Turnstile.SiteKey) ? null : _options.Turnstile.SiteKey.Trim(); + + public Task<(bool Allowed, string? ErrorCode, string? Message)> CheckAuthOtpByIpAsync( + string clientIp, + CancellationToken cancellationToken = default) => + CheckSlidingAsync($"abuse:otp-ip:{clientIp}", _options.RateLimits.AuthOtpPerIpPerHour, TimeSpan.FromHours(1), cancellationToken); + + public async Task<(bool Allowed, string? ErrorCode, string? Message)> CheckGuestOrderAsync( + string cafeId, + string clientIp, + CancellationToken cancellationToken = default) + { + if (!_options.Enabled) + return (true, null, null); + + var checks = new[] + { + ($"abuse:qr-ip:{cafeId}:{clientIp}", _options.GuestOrders.PerIpPerCafePerHour, TimeSpan.FromHours(1)), + ($"abuse:qr-cafe:{cafeId}", _options.GuestOrders.PerCafePerHour, TimeSpan.FromHours(1)), + ($"abuse:qr-ip-global:{clientIp}", _options.GuestOrders.PerIpGlobalPerHour, TimeSpan.FromHours(1)) + }; + + foreach (var (key, limit, window) in checks) + { + if (limit <= 0) continue; + var (allowed, _, _) = await CheckSlidingAsync(key, limit, window, cancellationToken); + if (!allowed) + return (false, "RATE_LIMITED", "Too many orders from this network. Please wait and try again."); + } + + return (true, null, null); + } + + public Task<(bool Allowed, string? ErrorCode, string? Message)> CheckPublicWriteByIpAsync( + string clientIp, + CancellationToken cancellationToken = default) => + CheckSlidingAsync( + $"abuse:pub-write:{clientIp}", + _options.RateLimits.PublicWritesPerIpPerMinute, + TimeSpan.FromMinutes(1), + cancellationToken); + + public async Task<(bool Ok, string? ErrorCode, string? Message)> VerifyCaptchaAsync( + string? captchaToken, + CancellationToken cancellationToken = default) + { + var required = _options.RequireCaptchaOnPublicWrites && IsCaptchaConfigured; + if (!required) + { + if (string.IsNullOrWhiteSpace(captchaToken) || !IsCaptchaConfigured) + return (true, null, null); + } + else if (string.IsNullOrWhiteSpace(captchaToken)) + { + return (false, "CAPTCHA_REQUIRED", "Complete the security check and try again."); + } + + if (!IsCaptchaConfigured) + return (true, null, null); + + try + { + var client = _httpClientFactory.CreateClient("turnstile"); + var response = await client.PostAsync( + "https://challenges.cloudflare.com/turnstile/v0/siteverify", + new FormUrlEncodedContent(new Dictionary + { + ["secret"] = _options.Turnstile.SecretKey, + ["response"] = captchaToken!.Trim() + }), + cancellationToken); + + var body = await response.Content.ReadFromJsonAsync(cancellationToken); + if (body?.Success == true) + return (true, null, null); + + _logger.LogWarning("Turnstile verification failed"); + return (false, "CAPTCHA_INVALID", "Security check failed. Refresh and try again."); + } + catch (Exception ex) + { + _logger.LogError(ex, "Turnstile verify error"); + return (false, "CAPTCHA_ERROR", "Could not verify security check."); + } + } + + private async Task<(bool Allowed, string? ErrorCode, string? Message)> CheckSlidingAsync( + string key, + int limit, + TimeSpan window, + CancellationToken cancellationToken) + { + if (!_options.Enabled || limit <= 0) + return (true, null, null); + + var db = _redis.GetDatabase(); + var count = await db.StringIncrementAsync(key); + if (count == 1) + await db.KeyExpireAsync(key, window); + + if ((int)count > limit) + return (false, "RATE_LIMITED", "Too many requests. Please slow down."); + + return (true, null, null); + } + + private sealed class TurnstileVerifyResponse + { + [JsonPropertyName("success")] + public bool Success { get; set; } + } +} diff --git a/src/Meezi.API/Security/ClientIpResolver.cs b/src/Meezi.API/Security/ClientIpResolver.cs new file mode 100644 index 0000000..f88e116 --- /dev/null +++ b/src/Meezi.API/Security/ClientIpResolver.cs @@ -0,0 +1,17 @@ +namespace Meezi.API.Security; + +public static class ClientIpResolver +{ + public static string GetClientIp(HttpContext context) + { + var forwarded = context.Request.Headers["X-Forwarded-For"].FirstOrDefault(); + if (!string.IsNullOrWhiteSpace(forwarded)) + { + var first = forwarded.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)[0]; + if (!string.IsNullOrWhiteSpace(first)) + return first; + } + + return context.Connection.RemoteIpAddress?.ToString() ?? "unknown"; + } +} diff --git a/src/Meezi.API/Security/IAbuseProtectionService.cs b/src/Meezi.API/Security/IAbuseProtectionService.cs new file mode 100644 index 0000000..1ccc64c --- /dev/null +++ b/src/Meezi.API/Security/IAbuseProtectionService.cs @@ -0,0 +1,24 @@ +namespace Meezi.API.Security; + +public interface IAbuseProtectionService +{ + bool IsCaptchaConfigured { get; } + string? CaptchaSiteKey { get; } + + Task<(bool Allowed, string? ErrorCode, string? Message)> CheckAuthOtpByIpAsync( + string clientIp, + CancellationToken cancellationToken = default); + + Task<(bool Allowed, string? ErrorCode, string? Message)> CheckGuestOrderAsync( + string cafeId, + string clientIp, + CancellationToken cancellationToken = default); + + Task<(bool Allowed, string? ErrorCode, string? Message)> CheckPublicWriteByIpAsync( + string clientIp, + CancellationToken cancellationToken = default); + + Task<(bool Ok, string? ErrorCode, string? Message)> VerifyCaptchaAsync( + string? captchaToken, + CancellationToken cancellationToken = default); +} diff --git a/src/Meezi.API/Security/PublicCafeGuard.cs b/src/Meezi.API/Security/PublicCafeGuard.cs new file mode 100644 index 0000000..c681f5f --- /dev/null +++ b/src/Meezi.API/Security/PublicCafeGuard.cs @@ -0,0 +1,17 @@ +using Meezi.Core.Entities; + +namespace Meezi.API.Security; + +public static class PublicCafeGuard +{ + public static (bool Ok, string? ErrorCode, string? Message) EnsureAcceptingPublicTraffic(Cafe cafe) + { + if (cafe.IsSuspended) + return (false, "CAFE_SUSPENDED", "This café is temporarily unavailable."); + + if (cafe.DeletedAt is not null) + return (false, "NOT_FOUND", "Cafe not found."); + + return (true, null, null); + } +} diff --git a/src/Meezi.API/Security/PublicWriteStatusCodes.cs b/src/Meezi.API/Security/PublicWriteStatusCodes.cs new file mode 100644 index 0000000..239e277 --- /dev/null +++ b/src/Meezi.API/Security/PublicWriteStatusCodes.cs @@ -0,0 +1,13 @@ +namespace Meezi.API.Security; + +public static class PublicWriteStatusCodes +{ + public static int ToHttpStatus(string? code) => code switch + { + "PLAN_LIMIT_REACHED" or "CAFE_SUSPENDED" => StatusCodes.Status403Forbidden, + "RATE_LIMITED" => StatusCodes.Status429TooManyRequests, + "CAPTCHA_REQUIRED" or "CAPTCHA_INVALID" or "CAPTCHA_ERROR" => StatusCodes.Status400BadRequest, + "NOT_FOUND" => StatusCodes.Status404NotFound, + _ => StatusCodes.Status400BadRequest + }; +} diff --git a/src/Meezi.API/Services/.gitkeep b/src/Meezi.API/Services/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/Meezi.API/Services/.gitkeep @@ -0,0 +1 @@ + diff --git a/src/Meezi.API/Services/AuthService.cs b/src/Meezi.API/Services/AuthService.cs new file mode 100644 index 0000000..8a5777a --- /dev/null +++ b/src/Meezi.API/Services/AuthService.cs @@ -0,0 +1,198 @@ +using Meezi.API.Models.Auth; +using Meezi.API.Security; +using Meezi.Core.Interfaces; +using Meezi.Core.Utilities; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using StackExchange.Redis; + +namespace Meezi.API.Services; + +public class AuthService : IAuthService +{ + private const int OtpTtlSeconds = 300; + private const int DefaultMaxOtpAttemptsPerHour = 5; + + private readonly AppDbContext _db; + private readonly IConnectionMultiplexer _redis; + private readonly ISmsService _smsService; + private readonly IJwtTokenService _jwtTokenService; + private readonly IRefreshTokenStore _refreshTokenStore; + private readonly IConfiguration _configuration; + private readonly ILogger _logger; + private readonly IAbuseProtectionService _abuse; + private readonly IHttpContextAccessor _http; + + public AuthService( + AppDbContext db, + IConnectionMultiplexer redis, + ISmsService smsService, + IJwtTokenService jwtTokenService, + IRefreshTokenStore refreshTokenStore, + IConfiguration configuration, + IAbuseProtectionService abuse, + IHttpContextAccessor http, + ILogger logger) + { + _db = db; + _redis = redis; + _smsService = smsService; + _jwtTokenService = jwtTokenService; + _refreshTokenStore = refreshTokenStore; + _configuration = configuration; + _abuse = abuse; + _http = http; + _logger = logger; + } + + public async Task<(bool Success, SendOtpResponse? Data, string? ErrorCode, string? ErrorMessage)> SendOtpAsync( + SendOtpRequest request, + CancellationToken cancellationToken = default) + { + var phone = PhoneNormalizer.Normalize(request.Phone); + var redis = _redis.GetDatabase(); + var maxAttempts = _configuration.GetValue("Auth:MaxOtpAttemptsPerHour", DefaultMaxOtpAttemptsPerHour); + + if (_http.HttpContext is not null) + { + var ip = ClientIpResolver.GetClientIp(_http.HttpContext); + var ipCheck = await _abuse.CheckAuthOtpByIpAsync(ip, cancellationToken); + if (!ipCheck.Allowed) + return (false, null, ipCheck.ErrorCode, ipCheck.Message); + } + + var employeeExists = await _db.Employees + .AnyAsync(e => e.Phone == phone && e.DeletedAt == null, cancellationToken); + + if (!employeeExists) + return (false, null, "NOT_FOUND", "No account found for this phone number."); + + var attemptsKey = $"otp:attempts:{phone}"; + if (maxAttempts > 0) + { + var attempts = await redis.StringGetAsync(attemptsKey); + if (attempts.HasValue && (int)attempts >= maxAttempts) + return (false, null, "RATE_LIMITED", "Too many OTP requests. Try again later."); + } + + var otp = Random.Shared.Next(100000, 999999).ToString(); + await redis.StringSetAsync($"otp:{phone}", otp, TimeSpan.FromSeconds(OtpTtlSeconds)); + + if (string.IsNullOrWhiteSpace(_configuration["Kavenegar:ApiKey"])) + _logger.LogWarning("DEV OTP for {Phone}: {Otp} (configure Kavenegar:ApiKey to send SMS)", phone, otp); + + try + { + await _smsService.SendOtpAsync(phone, otp, cancellationToken); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to send OTP SMS"); + return (false, null, "SMS_FAILED", "Could not send verification code."); + } + + if (maxAttempts > 0) + { + var newAttempts = await redis.StringIncrementAsync(attemptsKey); + if (newAttempts == 1) + await redis.KeyExpireAsync(attemptsKey, TimeSpan.FromHours(1)); + } + + _logger.LogInformation("OTP sent for phone ending {Suffix}", phone[^4..]); + + return (true, new SendOtpResponse(true, OtpTtlSeconds), null, null); + } + + public async Task<(bool Success, AuthTokenResponse? Data, string? ErrorCode, string? ErrorMessage)> VerifyOtpAsync( + VerifyOtpRequest request, + CancellationToken cancellationToken = default) + { + var phone = PhoneNormalizer.Normalize(request.Phone); + var code = OtpNormalizer.Normalize(request.Code); + if (!OtpNormalizer.IsValidSixDigitCode(code)) + return (false, null, "INVALID_OTP", "Invalid or expired verification code."); + + var redis = _redis.GetDatabase(); + + var storedOtp = await redis.StringGetAsync($"otp:{phone}"); + if (storedOtp.IsNullOrEmpty || storedOtp.ToString() != code) + return (false, null, "INVALID_OTP", "Invalid or expired verification code."); + + var query = _db.Employees + .Include(e => e.Cafe) + .Where(e => e.Phone == phone && e.DeletedAt == null); + + if (!string.IsNullOrWhiteSpace(request.CafeId)) + query = query.Where(e => e.CafeId == request.CafeId); + + var matches = await query.ToListAsync(cancellationToken); + if (matches.Count == 0) + return (false, null, "NOT_FOUND", "No account found for this phone number."); + if (matches.Count > 1) + return (false, null, "MULTIPLE_ACCOUNTS", "Multiple accounts use this phone. Contact your cafe owner."); + + var employee = matches[0]; + if (employee.Cafe is null) + return (false, null, "NOT_FOUND", "No account found for this phone number."); + + await redis.KeyDeleteAsync($"otp:{phone}"); + + var tokens = await IssueTokensAsync(employee, employee.Cafe, cancellationToken); + return (true, tokens, null, null); + } + + public async Task<(bool Success, AuthTokenResponse? Data, string? ErrorCode, string? ErrorMessage)> RefreshAsync( + RefreshTokenRequest request, + CancellationToken cancellationToken = default) + { + var payload = await _refreshTokenStore.GetAsync(request.RefreshToken, cancellationToken); + if (payload is null || payload.Actor == Meezi.Core.Constants.MeeziActorKinds.SystemAdmin) + return (false, null, "INVALID_TOKEN", "Refresh token is invalid or expired."); + + var employee = await _db.Employees + .Include(e => e.Cafe) + .FirstOrDefaultAsync(e => e.Id == payload.UserId && e.CafeId == payload.CafeId && e.DeletedAt == null, cancellationToken); + + if (employee?.Cafe is null) + return (false, null, "NOT_FOUND", "User no longer exists."); + + await _refreshTokenStore.RevokeAsync(request.RefreshToken, cancellationToken); + + var tokens = await IssueTokensAsync(employee, employee.Cafe, cancellationToken); + return (true, tokens, null, null); + } + + private async Task IssueTokensAsync( + Core.Entities.Employee employee, + Core.Entities.Cafe cafe, + CancellationToken cancellationToken) + { + var accessToken = _jwtTokenService.CreateAccessToken(employee, cafe); + var refreshToken = _jwtTokenService.CreateRefreshToken(); + var refreshDays = _configuration.GetValue("Jwt:RefreshTokenExpiryDays", 30); + + await _refreshTokenStore.StoreAsync( + refreshToken, + new RefreshTokenPayload( + employee.Id, + cafe.Id, + employee.Role.ToString(), + cafe.PlanTier.ToString(), + cafe.PreferredLanguage, + Meezi.Core.Constants.MeeziActorKinds.Merchant), + TimeSpan.FromDays(refreshDays), + cancellationToken); + + return new AuthTokenResponse( + accessToken, + refreshToken, + _jwtTokenService.GetAccessTokenExpiry(), + employee.Id, + cafe.Id, + employee.Role.ToString(), + cafe.PlanTier.ToString(), + cafe.PreferredLanguage, + Meezi.Core.Constants.MeeziActorKinds.Merchant, + employee.BranchId); + } +} diff --git a/src/Meezi.API/Services/BillingPaymentOrchestrator.cs b/src/Meezi.API/Services/BillingPaymentOrchestrator.cs new file mode 100644 index 0000000..eac3868 --- /dev/null +++ b/src/Meezi.API/Services/BillingPaymentOrchestrator.cs @@ -0,0 +1,156 @@ +using Meezi.API.Models.Billing; +using Meezi.Core.Enums; +using Meezi.Core.Interfaces; + +namespace Meezi.API.Services; + +public record PaymentInitResult(bool Success, string? Authority, string? PaymentUrl, string? ErrorMessage); + +public record PaymentVerifyResult(bool Success, string? RefId, string? ErrorMessage); + +public interface IBillingPaymentOrchestrator +{ + Task> GetEnabledMethodsAsync(CancellationToken cancellationToken = default); + Task InitiateAsync( + PaymentProvider provider, + long amountRials, + string paymentId, + string description, + string callbackUrl, + CancellationToken cancellationToken = default); + Task VerifyAsync( + PaymentProvider provider, + string externalId, + long amountRials, + CancellationToken cancellationToken = default); +} + +public class BillingPaymentOrchestrator : IBillingPaymentOrchestrator +{ + private readonly IZarinPalGateway _zarinPal; + private readonly ITaraPaymentGateway _tara; + private readonly ISnappPayGateway _snappPay; + private readonly IPlatformRuntimeConfig _platform; + + private static readonly (string Id, string NameFa)[] AllMethods = + [ + (PaymentProviderIds.ZarinPal, "زرین‌پال"), + (PaymentProviderIds.Tara, "تارا (اعتباری)"), + (PaymentProviderIds.SnappPay, "اسنپ‌پی (اقساطی)") + ]; + + public BillingPaymentOrchestrator( + IZarinPalGateway zarinPal, + ITaraPaymentGateway tara, + ISnappPayGateway snappPay, + IPlatformRuntimeConfig platform) + { + _zarinPal = zarinPal; + _tara = tara; + _snappPay = snappPay; + _platform = platform; + } + + public async Task> GetEnabledMethodsAsync( + CancellationToken cancellationToken = default) + { + var defaultId = await _platform.GetAsync("payment.activeGateway", cancellationToken) ?? PaymentProviderIds.ZarinPal; + var list = new List(); + + if (await _zarinPal.IsEnabledAsync(cancellationToken)) + list.Add(new PaymentMethodDto(PaymentProviderIds.ZarinPal, "زرین‌پال", defaultId == PaymentProviderIds.ZarinPal)); + if (await _tara.IsEnabledAsync(cancellationToken)) + list.Add(new PaymentMethodDto(PaymentProviderIds.Tara, "تارا (اعتباری)", defaultId == PaymentProviderIds.Tara)); + if (await _snappPay.IsEnabledAsync(cancellationToken)) + list.Add(new PaymentMethodDto(PaymentProviderIds.SnappPay, "اسنپ‌پی (اقساطی)", defaultId == PaymentProviderIds.SnappPay)); + + if (list.Count == 0) + list.Add(new PaymentMethodDto(PaymentProviderIds.ZarinPal, "زرین‌پال", true)); + + if (list.All(m => !m.IsDefault)) + list[0] = list[0] with { IsDefault = true }; + + return list; + } + + public async Task InitiateAsync( + PaymentProvider provider, + long amountRials, + string paymentId, + string description, + string callbackUrl, + CancellationToken cancellationToken = default) + { + return provider switch + { + PaymentProvider.Tara => await InitiateTaraAsync(amountRials, paymentId, callbackUrl, cancellationToken), + PaymentProvider.SnappPay => await InitiateSnappPayAsync(amountRials, paymentId, callbackUrl, cancellationToken), + _ => await InitiateZarinPalAsync(amountRials, description, callbackUrl, cancellationToken) + }; + } + + public async Task VerifyAsync( + PaymentProvider provider, + string externalId, + long amountRials, + CancellationToken cancellationToken = default) + { + return provider switch + { + PaymentProvider.Tara => + Map(await _tara.VerifyPaymentAsync(externalId, cancellationToken)), + PaymentProvider.SnappPay => + Map(await _snappPay.VerifyAndSettleAsync(externalId, cancellationToken)), + _ => + Map(await _zarinPal.VerifyPaymentAsync(externalId, amountRials, cancellationToken)) + }; + } + + private async Task InitiateZarinPalAsync( + long amountRials, + string description, + string callbackUrl, + CancellationToken cancellationToken) + { + if (!await _zarinPal.IsEnabledAsync(cancellationToken)) + return new PaymentInitResult(false, null, null, "ZarinPal is disabled."); + + var r = await _zarinPal.RequestPaymentAsync(amountRials, description, callbackUrl, cancellationToken); + return new PaymentInitResult(r.Success, r.Authority, r.PaymentUrl, r.ErrorMessage); + } + + private async Task InitiateTaraAsync( + long amountRials, + string paymentId, + string callbackUrl, + CancellationToken cancellationToken) + { + if (!await _tara.IsEnabledAsync(cancellationToken)) + return new PaymentInitResult(false, null, null, "Tara is disabled."); + + var r = await _tara.RequestPaymentAsync(amountRials, paymentId, callbackUrl, cancellationToken); + return new PaymentInitResult(r.Success, r.TraceNumber, r.PaymentUrl, r.ErrorMessage); + } + + private async Task InitiateSnappPayAsync( + long amountRials, + string paymentId, + string callbackUrl, + CancellationToken cancellationToken) + { + if (!await _snappPay.IsEnabledAsync(cancellationToken)) + return new PaymentInitResult(false, null, null, "Snapp Pay is disabled."); + + var r = await _snappPay.RequestPaymentAsync(amountRials, paymentId, callbackUrl, cancellationToken); + return new PaymentInitResult(r.Success, r.PaymentToken, r.PaymentUrl, r.ErrorMessage); + } + + private static PaymentVerifyResult Map(ZarinPalVerifyResult r) => + new(r.Success, r.RefId, r.ErrorMessage); + + private static PaymentVerifyResult Map(TaraVerifyResult r) => + new(r.Success, r.RefId, r.ErrorMessage); + + private static PaymentVerifyResult Map(SnappPayVerifyResult r) => + new(r.Success, r.RefId, r.ErrorMessage); +} diff --git a/src/Meezi.API/Services/BillingService.cs b/src/Meezi.API/Services/BillingService.cs new file mode 100644 index 0000000..b2ad3d9 --- /dev/null +++ b/src/Meezi.API/Services/BillingService.cs @@ -0,0 +1,307 @@ +using Meezi.API.Models.Billing; +using Meezi.Infrastructure.Services.Platform; +using Meezi.Core.Constants; +using Meezi.Core.Entities; +using Meezi.Core.Enums; +using Meezi.Core.Interfaces; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using StackExchange.Redis; + +namespace Meezi.API.Services; + +public interface IBillingService +{ + Task> GetPaymentMethodsAsync(CancellationToken cancellationToken = default); + + Task<(SubscribeResponse? Data, string? ErrorCode, string? Message)> InitiateSubscriptionAsync( + string cafeId, + SubscribeRequest request, + CancellationToken cancellationToken = default); + + Task VerifyZarinPalAsync( + string authority, + string? status, + CancellationToken cancellationToken = default); + + Task VerifySnappPayAsync( + string? paymentToken, + string? state, + CancellationToken cancellationToken = default); + + Task VerifyTaraAsync( + string? traceNumber, + string? status, + CancellationToken cancellationToken = default); + + Task GetStatusAsync(string cafeId, PlanTier currentTier, CancellationToken cancellationToken = default); +} + +public class BillingService : IBillingService +{ + private readonly AppDbContext _db; + private readonly IBillingPaymentOrchestrator _payments; + private readonly ISmsService _smsService; + private readonly IConnectionMultiplexer _redis; + private readonly IConfiguration _configuration; + private readonly IPlatformCatalogService _platformCatalog; + private readonly ILogger _logger; + + public BillingService( + AppDbContext db, + IBillingPaymentOrchestrator payments, + ISmsService smsService, + IConnectionMultiplexer redis, + IConfiguration configuration, + IPlatformCatalogService platformCatalog, + ILogger logger) + { + _db = db; + _payments = payments; + _smsService = smsService; + _redis = redis; + _configuration = configuration; + _platformCatalog = platformCatalog; + _logger = logger; + } + + private const string FeatureMenu3d = "menu_3d"; + private const string FeatureMenu3dAi = "menu_3d_ai"; + private const string FeatureDiscoverProfile = "discover_profile"; + + public async Task<(SubscribeResponse? Data, string? ErrorCode, string? Message)> InitiateSubscriptionAsync( + string cafeId, + SubscribeRequest request, + CancellationToken cancellationToken = default) + { + var cafe = await _db.Cafes.FirstOrDefaultAsync(c => c.Id == cafeId, cancellationToken); + if (cafe is null) + return (null, "NOT_FOUND", "Cafe not found."); + + if (!await _platformCatalog.IsBillableOnlineAsync(request.PlanTier, cancellationToken)) + return (null, "NOT_BILLABLE", "This plan requires contacting sales."); + + var monthly = await _platformCatalog.GetMonthlyPriceTomanAsync(request.PlanTier, cancellationToken); + if (monthly <= 0) + return (null, "NOT_BILLABLE", "This plan has no online price."); + + var amountToman = monthly * request.Months; + var amountRials = PlanPricing.ToRials(amountToman); + + var methods = await _payments.GetEnabledMethodsAsync(cancellationToken); + var methodId = string.IsNullOrWhiteSpace(request.PaymentMethod) + ? methods.FirstOrDefault(m => m.IsDefault)?.Id ?? PaymentProviderIds.ZarinPal + : request.PaymentMethod.Trim().ToLowerInvariant(); + + var provider = PaymentProviderIds.Parse(methodId); + if (provider is null || methods.All(m => m.Id != methodId)) + return (null, "PAYMENT_METHOD_DISABLED", "Selected payment method is not available."); + + var payment = new SubscriptionPayment + { + CafeId = cafeId, + PlanTier = request.PlanTier, + Months = request.Months, + AmountToman = amountToman, + AmountRials = amountRials, + Provider = provider.Value, + Status = SubscriptionPaymentStatus.Pending + }; + + _db.SubscriptionPayments.Add(payment); + await _db.SaveChangesAsync(cancellationToken); + + var apiBase = _configuration["App:PublicBaseUrl"]?.TrimEnd('/') ?? "http://localhost:5080"; + var callbackUrl = provider.Value switch + { + PaymentProvider.SnappPay => $"{apiBase}/api/billing/verify/snapppay", + PaymentProvider.Tara => $"{apiBase}/api/billing/verify/tara", + _ => $"{apiBase}/api/billing/verify" + }; + + var description = $"میزی — اشتراک {request.PlanTier} ({request.Months} ماه)"; + var init = await _payments.InitiateAsync( + provider.Value, + amountRials, + payment.Id, + description, + callbackUrl, + cancellationToken); + + if (!init.Success || string.IsNullOrEmpty(init.Authority) || string.IsNullOrEmpty(init.PaymentUrl)) + { + payment.Status = SubscriptionPaymentStatus.Failed; + await _db.SaveChangesAsync(cancellationToken); + return (null, "PAYMENT_FAILED", init.ErrorMessage ?? "Could not start payment."); + } + + payment.Authority = init.Authority; + await _db.SaveChangesAsync(cancellationToken); + + return (new SubscribeResponse(payment.Id, init.PaymentUrl), null, null); + } + + public Task> GetPaymentMethodsAsync(CancellationToken cancellationToken = default) => + _payments.GetEnabledMethodsAsync(cancellationToken); + + public Task VerifyZarinPalAsync( + string authority, + string? status, + CancellationToken cancellationToken = default) => + CompletePaymentAsync( + PaymentProvider.ZarinPal, + authority, + status is null or "" or "OK", + cancellationToken); + + public Task VerifySnappPayAsync( + string? paymentToken, + string? state, + CancellationToken cancellationToken = default) => + CompletePaymentAsync( + PaymentProvider.SnappPay, + paymentToken, + !string.IsNullOrWhiteSpace(paymentToken) + && (string.IsNullOrWhiteSpace(state) || state.Equals("OK", StringComparison.OrdinalIgnoreCase)), + cancellationToken); + + public Task VerifyTaraAsync( + string? traceNumber, + string? status, + CancellationToken cancellationToken = default) => + CompletePaymentAsync( + PaymentProvider.Tara, + traceNumber, + status is null or "" or "OK", + cancellationToken); + + private async Task CompletePaymentAsync( + PaymentProvider provider, + string? externalId, + bool callbackOk, + CancellationToken cancellationToken) + { + var dashboardBase = _configuration["Billing:DashboardBaseUrl"]?.TrimEnd('/') + ?? _configuration.GetSection("Cors:Origins").Get()?.FirstOrDefault() + ?? "http://localhost:3101"; + var failUrl = $"{dashboardBase}/fa/subscription?billing=failed"; + var successUrl = $"{dashboardBase}/fa/subscription?billing=success"; + + if (!callbackOk || string.IsNullOrWhiteSpace(externalId)) + return new BillingVerifyResult(false, failUrl); + + var payment = await _db.SubscriptionPayments + .Include(p => p.Cafe) + .FirstOrDefaultAsync( + p => p.Authority == externalId && p.Provider == provider, + cancellationToken); + + if (payment is null) + return new BillingVerifyResult(false, failUrl); + + if (payment.Status == SubscriptionPaymentStatus.Completed) + return new BillingVerifyResult(true, successUrl); + + var verify = await _payments.VerifyAsync(provider, externalId, payment.AmountRials, cancellationToken); + if (!verify.Success) + { + payment.Status = SubscriptionPaymentStatus.Failed; + await _db.SaveChangesAsync(cancellationToken); + return new BillingVerifyResult(false, failUrl); + } + + payment.Status = SubscriptionPaymentStatus.Completed; + payment.RefId = verify.RefId; + + var cafe = payment.Cafe; + cafe.PlanTier = payment.PlanTier; + var baseDate = cafe.PlanExpiresAt.HasValue && cafe.PlanExpiresAt > DateTime.UtcNow + ? cafe.PlanExpiresAt.Value + : DateTime.UtcNow; + cafe.PlanExpiresAt = baseDate.AddMonths(payment.Months); + + await _db.SaveChangesAsync(cancellationToken); + + await TrySendConfirmationSmsAsync(cafe, payment, cancellationToken); + + return new BillingVerifyResult(true, successUrl); + } + + public async Task GetStatusAsync( + string cafeId, + PlanTier currentTier, + CancellationToken cancellationToken = default) + { + var cafe = await _db.Cafes.AsNoTracking().FirstOrDefaultAsync(c => c.Id == cafeId, cancellationToken); + if (cafe is null) return null; + + var todayStart = DateTime.UtcNow.Date; + var ordersToday = await _db.Orders.CountAsync( + o => o.CafeId == cafeId && o.CreatedAt >= todayStart, + cancellationToken); + + var customersCount = await _db.Customers.CountAsync(c => c.CafeId == cafeId, cancellationToken); + + var maxOrders = PlanLimits.MaxOrdersPerDay(cafe.PlanTier); + var maxCustomers = PlanLimits.MaxCustomers(cafe.PlanTier); + var maxSms = PlanLimits.MaxSmsPerMonth(cafe.PlanTier); + + var monthKey = $"sms:usage:{cafeId}:{DateTime.UtcNow:yyyy-MM}"; + var redis = _redis.GetDatabase(); + var smsUsed = await redis.StringGetAsync(monthKey); + var smsUsedCount = smsUsed.HasValue ? (int)smsUsed : 0; + + var menu3d = await _platformCatalog.IsFeatureEnabledForCafeAsync( + cafeId, cafe.PlanTier, FeatureMenu3d, cancellationToken); + var menuAi3d = await _platformCatalog.IsFeatureEnabledForCafeAsync( + cafeId, cafe.PlanTier, FeatureMenu3dAi, cancellationToken); + var discoverProfile = await _platformCatalog.IsFeatureEnabledForCafeAsync( + cafeId, cafe.PlanTier, FeatureDiscoverProfile, cancellationToken); + var isExpired = cafe.PlanExpiresAt.HasValue && cafe.PlanExpiresAt.Value < DateTime.UtcNow; + + var ai3dKey = $"ai3d:usage:{cafeId}:{DateTime.UtcNow:yyyy-MM}"; + var ai3dUsed = await redis.StringGetAsync(ai3dKey); + var ai3dUsedCount = ai3dUsed.HasValue && int.TryParse(ai3dUsed.ToString(), out var aiN) ? aiN : 0; + var ai3dLimit = menuAi3d ? PlanLimits.MaxMenuAi3dPerMonth(cafe.PlanTier) : 0; + + return new BillingStatusDto( + cafe.PlanTier, + cafe.PlanExpiresAt, + ordersToday, + maxOrders == int.MaxValue ? null : maxOrders, + customersCount, + maxCustomers == int.MaxValue ? null : maxCustomers, + smsUsedCount, + maxSms == int.MaxValue ? -1 : maxSms, + menu3d, + menuAi3d, + ai3dUsedCount, + ai3dLimit, + discoverProfile, + isExpired); + } + + private async Task TrySendConfirmationSmsAsync( + Cafe cafe, + SubscriptionPayment payment, + CancellationToken cancellationToken) + { + var ownerPhone = await _db.Employees + .Where(e => e.CafeId == cafe.Id && e.Role == EmployeeRole.Owner) + .Select(e => e.Phone) + .FirstOrDefaultAsync(cancellationToken); + + if (string.IsNullOrEmpty(ownerPhone)) return; + + var message = + $"میزی: اشتراک {payment.PlanTier} فعال شد تا {cafe.PlanExpiresAt:yyyy-MM-dd}. مبلغ: {payment.AmountToman:N0} ت"; + try + { + await _smsService.SendMessageAsync(ownerPhone, message, cancellationToken); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to send subscription confirmation SMS for cafe {CafeId}", cafe.Id); + } + } +} diff --git a/src/Meezi.API/Services/BranchIdentityService.cs b/src/Meezi.API/Services/BranchIdentityService.cs new file mode 100644 index 0000000..91ff269 --- /dev/null +++ b/src/Meezi.API/Services/BranchIdentityService.cs @@ -0,0 +1,81 @@ +using Meezi.API.Models.Cafes; +using Meezi.API.Models.Public; +using Meezi.Infrastructure.Branding; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; + +namespace Meezi.API.Services; + +public interface IBranchIdentityService +{ + Task GetEffectiveIdentityAsync( + string cafeId, + string branchId, + CancellationToken cancellationToken = default); +} + +public record BranchEffectiveIdentityDto( + string PrimaryColor, + string SecondaryColor, + string? FontFamily, + string? LogoUrl, + string? IconName, + string WelcomeText, + string? WifiPassword, + string? Address, + string? AccentColor); + +public class BranchIdentityService : IBranchIdentityService +{ + private const string DefaultPrimary = "#0F6E56"; + private const string DefaultSecondary = "#E1F5EE"; + + private readonly AppDbContext _db; + + public BranchIdentityService(AppDbContext db) + { + _db = db; + } + + public async Task GetEffectiveIdentityAsync( + string cafeId, + string branchId, + CancellationToken cancellationToken = default) + { + var branch = await _db.Branches + .Include(b => b.Cafe) + .FirstOrDefaultAsync( + b => b.Id == branchId && b.CafeId == cafeId && b.IsActive, + cancellationToken); + + if (branch?.Cafe is null) return null; + + var theme = CafeThemeSerializer.Parse(branch.Cafe.ThemeJson); + var primary = theme.Custom?.Primary?.Trim(); + if (string.IsNullOrEmpty(primary)) + primary = DefaultPrimary; + + var secondary = theme.Custom?.Secondary?.Trim(); + if (string.IsNullOrEmpty(secondary)) + secondary = DefaultSecondary; + + var logo = !string.IsNullOrWhiteSpace(branch.LogoUrl) + ? branch.LogoUrl + : branch.Cafe.LogoUrl; + + var welcome = !string.IsNullOrWhiteSpace(branch.WelcomeText) + ? branch.WelcomeText.Trim() + : "خوش آمدید"; + + return new BranchEffectiveIdentityDto( + primary, + secondary, + null, + logo, + "coffee", + welcome, + branch.WifiPassword, + branch.Address ?? branch.Cafe.Address, + branch.AccentColor); + } +} diff --git a/src/Meezi.API/Services/BranchLifecycleService.cs b/src/Meezi.API/Services/BranchLifecycleService.cs new file mode 100644 index 0000000..44c5bdf --- /dev/null +++ b/src/Meezi.API/Services/BranchLifecycleService.cs @@ -0,0 +1,246 @@ +using Microsoft.EntityFrameworkCore; +using Meezi.Core.Entities; +using Meezi.Infrastructure.Data; + +namespace Meezi.API.Services; + +public interface IBranchLifecycleService +{ + Task<(bool Ok, string? ErrorCode, string? Message)> ScheduleDeletionAsync( + string cafeId, + string branchId, + CancellationToken cancellationToken = default); + + Task<(bool Ok, string? ErrorCode, string? Message)> RestoreAsync( + string cafeId, + string branchId, + CancellationToken cancellationToken = default); + + Task PurgeExpiredDeletionsAsync(CancellationToken cancellationToken = default); +} + +public class BranchLifecycleService : IBranchLifecycleService +{ + public const int RecoveryDays = 7; + + private readonly AppDbContext _db; + private readonly ILogger _logger; + + public BranchLifecycleService(AppDbContext db, ILogger logger) + { + _db = db; + _logger = logger; + } + + public async Task<(bool Ok, string? ErrorCode, string? Message)> ScheduleDeletionAsync( + string cafeId, + string branchId, + CancellationToken cancellationToken = default) + { + var branch = await _db.Branches + .FirstOrDefaultAsync(b => b.Id == branchId && b.CafeId == cafeId, cancellationToken); + if (branch is null) + return (false, "NOT_FOUND", "Branch not found."); + if (branch.DeletedAt is not null) + return (false, "ALREADY_DELETED", "Branch is already scheduled for removal."); + + var activeCount = await _db.Branches.CountAsync( + b => b.CafeId == cafeId && b.DeletedAt == null, + cancellationToken); + if (activeCount <= 1) + return (false, "LAST_BRANCH", "At least one active branch is required."); + + var now = DateTime.UtcNow; + var purgeAt = now.AddDays(RecoveryDays); + branch.DeletedAt = now; + branch.ScheduledPermanentDeleteAt = purgeAt; + branch.IsActive = false; + branch.UpdatedAt = now; + + await SoftDeleteBranchScopedDataAsync(cafeId, branchId, now, cancellationToken); + await _db.SaveChangesAsync(cancellationToken); + return (true, null, null); + } + + public async Task<(bool Ok, string? ErrorCode, string? Message)> RestoreAsync( + string cafeId, + string branchId, + CancellationToken cancellationToken = default) + { + var branch = await _db.Branches + .IgnoreQueryFilters() + .FirstOrDefaultAsync(b => b.Id == branchId && b.CafeId == cafeId, cancellationToken); + if (branch is null || branch.DeletedAt is null) + return (false, "NOT_FOUND", "Branch is not pending deletion."); + if (branch.ScheduledPermanentDeleteAt is not null + && branch.ScheduledPermanentDeleteAt <= DateTime.UtcNow) + return (false, "PURGE_EXPIRED", "Recovery period has ended."); + + var deletedAt = branch.DeletedAt.Value; + branch.DeletedAt = null; + branch.ScheduledPermanentDeleteAt = null; + branch.IsActive = true; + branch.UpdatedAt = DateTime.UtcNow; + + await RestoreBranchScopedDataAsync(cafeId, branchId, deletedAt, cancellationToken); + await _db.SaveChangesAsync(cancellationToken); + return (true, null, null); + } + + public async Task PurgeExpiredDeletionsAsync(CancellationToken cancellationToken = default) + { + var now = DateTime.UtcNow; + var expired = await _db.Branches + .IgnoreQueryFilters() + .Where(b => b.DeletedAt != null && b.ScheduledPermanentDeleteAt != null && b.ScheduledPermanentDeleteAt <= now) + .Select(b => new { b.Id, b.CafeId }) + .ToListAsync(cancellationToken); + + var purged = 0; + foreach (var b in expired) + { + try + { + await HardDeleteBranchAsync(b.CafeId, b.Id, cancellationToken); + purged++; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to permanently delete branch {BranchId}", b.Id); + } + } + + return purged; + } + + private async Task SoftDeleteBranchScopedDataAsync( + string cafeId, + string branchId, + DateTime deletedAt, + CancellationToken cancellationToken) + { + await _db.Tables + .Where(t => t.CafeId == cafeId && t.BranchId == branchId && t.DeletedAt == null) + .ExecuteUpdateAsync(s => s + .SetProperty(t => t.DeletedAt, deletedAt) + .SetProperty(t => t.IsActive, false), + cancellationToken); + + await _db.TableSections + .Where(s => s.CafeId == cafeId && s.BranchId == branchId && s.DeletedAt == null) + .ExecuteUpdateAsync(s => s.SetProperty(x => x.DeletedAt, deletedAt), cancellationToken); + + await _db.Employees + .Where(e => e.CafeId == cafeId && e.BranchId == branchId && e.DeletedAt == null) + .ExecuteUpdateAsync(s => s.SetProperty(e => e.DeletedAt, deletedAt), cancellationToken); + + await _db.BranchMenuItemOverrides + .Where(o => o.CafeId == cafeId && o.BranchId == branchId && o.DeletedAt == null) + .ExecuteUpdateAsync(s => s.SetProperty(o => o.DeletedAt, deletedAt), cancellationToken); + + await _db.RegisterShifts + .Where(s => s.CafeId == cafeId && s.BranchId == branchId && s.DeletedAt == null) + .ExecuteUpdateAsync(s => s.SetProperty(s => s.DeletedAt, deletedAt), cancellationToken); + + await _db.Expenses + .Where(e => e.CafeId == cafeId && e.BranchId == branchId && e.DeletedAt == null) + .ExecuteUpdateAsync(s => s.SetProperty(e => e.DeletedAt, deletedAt), cancellationToken); + + await _db.DailyReports + .Where(r => r.CafeId == cafeId && r.BranchId == branchId && r.DeletedAt == null) + .ExecuteUpdateAsync(s => s.SetProperty(r => r.DeletedAt, deletedAt), cancellationToken); + } + + private async Task RestoreBranchScopedDataAsync( + string cafeId, + string branchId, + DateTime deletedAt, + CancellationToken cancellationToken) + { + await _db.Tables + .IgnoreQueryFilters() + .Where(t => t.CafeId == cafeId && t.BranchId == branchId && t.DeletedAt == deletedAt) + .ExecuteUpdateAsync(s => s + .SetProperty(t => t.DeletedAt, (DateTime?)null) + .SetProperty(t => t.IsActive, true), + cancellationToken); + + await _db.TableSections + .IgnoreQueryFilters() + .Where(s => s.CafeId == cafeId && s.BranchId == branchId && s.DeletedAt == deletedAt) + .ExecuteUpdateAsync(s => s.SetProperty(x => x.DeletedAt, (DateTime?)null), cancellationToken); + + await _db.Employees + .IgnoreQueryFilters() + .Where(e => e.CafeId == cafeId && e.BranchId == branchId && e.DeletedAt == deletedAt) + .ExecuteUpdateAsync(s => s.SetProperty(e => e.DeletedAt, (DateTime?)null), cancellationToken); + + await _db.BranchMenuItemOverrides + .IgnoreQueryFilters() + .Where(o => o.CafeId == cafeId && o.BranchId == branchId && o.DeletedAt == deletedAt) + .ExecuteUpdateAsync(s => s.SetProperty(o => o.DeletedAt, (DateTime?)null), cancellationToken); + + await _db.RegisterShifts + .IgnoreQueryFilters() + .Where(s => s.CafeId == cafeId && s.BranchId == branchId && s.DeletedAt == deletedAt) + .ExecuteUpdateAsync(s => s.SetProperty(s => s.DeletedAt, (DateTime?)null), cancellationToken); + + await _db.Expenses + .IgnoreQueryFilters() + .Where(e => e.CafeId == cafeId && e.BranchId == branchId && e.DeletedAt == deletedAt) + .ExecuteUpdateAsync(s => s.SetProperty(e => e.DeletedAt, (DateTime?)null), cancellationToken); + + await _db.DailyReports + .IgnoreQueryFilters() + .Where(r => r.CafeId == cafeId && r.BranchId == branchId && r.DeletedAt == deletedAt) + .ExecuteUpdateAsync(s => s.SetProperty(r => r.DeletedAt, (DateTime?)null), cancellationToken); + } + + private async Task HardDeleteBranchAsync( + string cafeId, + string branchId, + CancellationToken cancellationToken) + { + await _db.BranchMenuItemOverrides + .IgnoreQueryFilters() + .Where(o => o.CafeId == cafeId && o.BranchId == branchId) + .ExecuteDeleteAsync(cancellationToken); + + await _db.Tables + .IgnoreQueryFilters() + .Where(t => t.CafeId == cafeId && t.BranchId == branchId) + .ExecuteDeleteAsync(cancellationToken); + + await _db.TableSections + .IgnoreQueryFilters() + .Where(s => s.CafeId == cafeId && s.BranchId == branchId) + .ExecuteDeleteAsync(cancellationToken); + + await _db.RegisterShifts + .IgnoreQueryFilters() + .Where(s => s.CafeId == cafeId && s.BranchId == branchId) + .ExecuteDeleteAsync(cancellationToken); + + await _db.Expenses + .IgnoreQueryFilters() + .Where(e => e.CafeId == cafeId && e.BranchId == branchId) + .ExecuteDeleteAsync(cancellationToken); + + await _db.DailyReports + .IgnoreQueryFilters() + .Where(r => r.CafeId == cafeId && r.BranchId == branchId) + .ExecuteDeleteAsync(cancellationToken); + + await _db.Employees + .IgnoreQueryFilters() + .Where(e => e.CafeId == cafeId && e.BranchId == branchId) + .ExecuteDeleteAsync(cancellationToken); + + await _db.Branches + .IgnoreQueryFilters() + .Where(b => b.Id == branchId && b.CafeId == cafeId) + .ExecuteDeleteAsync(cancellationToken); + + _logger.LogInformation("Permanently deleted branch {BranchId} for cafe {CafeId}", branchId, cafeId); + } +} diff --git a/src/Meezi.API/Services/BranchMenuService.cs b/src/Meezi.API/Services/BranchMenuService.cs new file mode 100644 index 0000000..96613fa --- /dev/null +++ b/src/Meezi.API/Services/BranchMenuService.cs @@ -0,0 +1,185 @@ +using Meezi.API.Models.Menu; +using Meezi.Core.Entities; +using Meezi.Core.Enums; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; + +namespace Meezi.API.Services; + +public interface IBranchMenuService +{ + Task?> GetBranchMenuAsync( + string cafeId, + string branchId, + bool includeUnavailable, + CancellationToken cancellationToken = default); + + Task<(bool Success, BranchMenuOverrideDto? Data, string? ErrorCode, string? Message)> UpsertOverrideAsync( + string cafeId, + string branchId, + string menuItemId, + UpsertBranchMenuOverrideRequest request, + PlanTier planTier, + EmployeeRole? role, + string? userId, + CancellationToken cancellationToken = default); + + Task DeleteOverrideAsync( + string cafeId, + string branchId, + string menuItemId, + CancellationToken cancellationToken = default); +} + +public class BranchMenuService : IBranchMenuService +{ + private readonly AppDbContext _db; + + public BranchMenuService(AppDbContext db) + { + _db = db; + } + + public async Task?> GetBranchMenuAsync( + string cafeId, + string branchId, + bool includeUnavailable, + CancellationToken cancellationToken = default) + { + var branchOk = await _db.Branches.AnyAsync( + b => b.Id == branchId && b.CafeId == cafeId, + cancellationToken); + if (!branchOk) return null; + + var items = await _db.MenuItems + .Include(i => i.Category) + .Where(i => i.CafeId == cafeId && i.Category.IsActive) + .OrderBy(i => i.Category.SortOrder) + .ThenBy(i => i.Name) + .ToListAsync(cancellationToken); + + var overrides = await _db.BranchMenuItemOverrides + .Where(o => o.CafeId == cafeId && o.BranchId == branchId) + .ToDictionaryAsync(o => o.MenuItemId, cancellationToken); + + var result = new List(); + + foreach (var item in items) + { + overrides.TryGetValue(item.Id, out var ov); + var branchAvailable = ov?.IsAvailable ?? true; + var catalogAvailable = item.IsAvailable; + + if (!includeUnavailable) + { + if (!catalogAvailable || !branchAvailable) continue; + } + else if (!catalogAvailable && ov is null) + { + continue; + } + + var masterPrice = item.Price; + var effectivePrice = ov?.PriceOverride ?? masterPrice; + var hasPriceOverride = ov?.PriceOverride is not null; + + result.Add(new BranchMenuItemDto( + item.Id, + item.CategoryId, + item.Name, + item.NameAr, + item.NameEn, + item.Description, + masterPrice, + effectivePrice, + item.DiscountPercent, + item.ImageUrl, + item.VideoUrl, + item.Model3dUrl, + branchAvailable && catalogAvailable, + ov is not null, + hasPriceOverride)); + } + + return result; + } + + public async Task<(bool Success, BranchMenuOverrideDto? Data, string? ErrorCode, string? Message)> UpsertOverrideAsync( + string cafeId, + string branchId, + string menuItemId, + UpsertBranchMenuOverrideRequest request, + PlanTier planTier, + EmployeeRole? role, + string? userId, + CancellationToken cancellationToken = default) + { + if (request.PriceOverride is not null && !CanSetPriceOverride(planTier, role)) + return (false, null, "PLAN_LIMIT_REACHED", + "قیمت‌گذاری شعبه‌ای نیاز به پلن Pro دارد"); + + var branchOk = await _db.Branches.AnyAsync( + b => b.Id == branchId && b.CafeId == cafeId, + cancellationToken); + if (!branchOk) return (false, null, "BRANCH_NOT_FOUND", "Branch not found."); + + var item = await _db.MenuItems.FirstOrDefaultAsync( + i => i.Id == menuItemId && i.CafeId == cafeId, + cancellationToken); + if (item is null) return (false, null, "NOT_FOUND", "Menu item not found."); + + if (request.PriceOverride is < 0) + return (false, null, "VALIDATION_ERROR", "Price override must be non-negative."); + + var existing = await _db.BranchMenuItemOverrides.FirstOrDefaultAsync( + o => o.BranchId == branchId && o.MenuItemId == menuItemId, + cancellationToken); + + if (existing is null) + { + existing = new BranchMenuItemOverride + { + CafeId = cafeId, + BranchId = branchId, + MenuItemId = menuItemId, + }; + _db.BranchMenuItemOverrides.Add(existing); + } + + existing.IsAvailable = request.IsAvailable; + existing.PriceOverride = request.PriceOverride; + existing.UpdatedAt = DateTime.UtcNow; + existing.UpdatedByUserId = userId; + + await _db.SaveChangesAsync(cancellationToken); + + return (true, new BranchMenuOverrideDto( + menuItemId, + branchId, + existing.IsAvailable, + existing.PriceOverride, + existing.UpdatedAt), null, null); + } + + public async Task DeleteOverrideAsync( + string cafeId, + string branchId, + string menuItemId, + CancellationToken cancellationToken = default) + { + var row = await _db.BranchMenuItemOverrides.FirstOrDefaultAsync( + o => o.CafeId == cafeId && o.BranchId == branchId && o.MenuItemId == menuItemId, + cancellationToken); + if (row is null) return false; + + _db.BranchMenuItemOverrides.Remove(row); + await _db.SaveChangesAsync(cancellationToken); + return true; + } + + internal static bool CanSetPriceOverride(PlanTier planTier, EmployeeRole? role) => + role is EmployeeRole.Owner || planTier is not PlanTier.Free; + + internal static bool CanManageOverrides(EmployeeRole? role) => + role is EmployeeRole.Owner or EmployeeRole.Manager; +} diff --git a/src/Meezi.API/Services/CafeDiscoverProfileMapping.cs b/src/Meezi.API/Services/CafeDiscoverProfileMapping.cs new file mode 100644 index 0000000..757ad54 --- /dev/null +++ b/src/Meezi.API/Services/CafeDiscoverProfileMapping.cs @@ -0,0 +1,43 @@ +using Meezi.API.Models.Discover; +using Meezi.Core.Discover; +using Meezi.Infrastructure.Discover; + +namespace Meezi.API.Services; + +public static class CafeDiscoverProfileMapping +{ + public static CafeDiscoverProfileDto ToDto(CafeDiscoverProfile profile) => + new( + profile.Themes, + profile.Size, + profile.Floors, + profile.Vibes, + profile.Occasions, + profile.SpaceFeatures, + profile.NoiseLevel, + profile.PriceTier); + + public static CafeDiscoverProfile FromRequest(UpsertCafeDiscoverProfileRequest request) => + CafeDiscoverProfileSerializer.Sanitize(new CafeDiscoverProfile + { + Themes = request.Themes?.ToList() ?? [], + Size = request.Size, + Floors = request.Floors, + Vibes = request.Vibes?.ToList() ?? [], + Occasions = request.Occasions?.ToList() ?? [], + SpaceFeatures = request.SpaceFeatures?.ToList() ?? [], + NoiseLevel = request.NoiseLevel, + PriceTier = request.PriceTier + }); + + public static DiscoverProfileTaxonomyDto Taxonomy() => + new( + CafeDiscoverProfileKeys.Themes.OrderBy(x => x).ToList(), + CafeDiscoverProfileKeys.Sizes.OrderBy(x => x).ToList(), + CafeDiscoverProfileKeys.Floors.OrderBy(x => x).ToList(), + CafeDiscoverProfileKeys.Vibes.OrderBy(x => x).ToList(), + CafeDiscoverProfileKeys.Occasions.OrderBy(x => x).ToList(), + CafeDiscoverProfileKeys.SpaceFeatures.OrderBy(x => x).ToList(), + CafeDiscoverProfileKeys.NoiseLevels.OrderBy(x => x).ToList(), + CafeDiscoverProfileKeys.PriceTiers.OrderBy(x => x).ToList()); +} diff --git a/src/Meezi.API/Services/CafeThemeMapping.cs b/src/Meezi.API/Services/CafeThemeMapping.cs new file mode 100644 index 0000000..9ae2a0a --- /dev/null +++ b/src/Meezi.API/Services/CafeThemeMapping.cs @@ -0,0 +1,76 @@ +using Meezi.API.Models.Cafes; +using Meezi.Core.Branding; +using Meezi.Infrastructure.Branding; + +namespace Meezi.API.Services; + +public static class CafeThemeMapping +{ + public static CafeThemeDto ToDto(CafeTheme theme) => new( + theme.PaletteId, + theme.PanelStyle, + theme.MenuStyle, + theme.MenuTexture, + theme.Density, + theme.Radius, + theme.Custom is null + ? null + : new CafeThemeCustomColorsDto( + theme.Custom.Primary, + theme.Custom.Secondary, + theme.Custom.Accent, + theme.Custom.Background, + theme.Custom.Surface, + theme.Custom.Text, + theme.Custom.TextMuted, + theme.Custom.Destructive, + theme.Custom.Success, + theme.Custom.PrimaryOpacity, + theme.Custom.SecondaryOpacity, + theme.Custom.AccentOpacity, + theme.Custom.BackgroundOpacity, + theme.Custom.SurfaceOpacity, + theme.Custom.TextOpacity, + theme.Custom.TextMutedOpacity, + theme.Custom.DestructiveOpacity, + theme.Custom.SuccessOpacity)); + + public static CafeTheme FromDto(CafeThemeDto dto) + { + var theme = new CafeTheme + { + PaletteId = dto.PaletteId, + PanelStyle = dto.PanelStyle, + MenuStyle = dto.MenuStyle, + MenuTexture = dto.MenuTexture, + Density = dto.Density, + Radius = dto.Radius, + Custom = dto.Custom is null + ? null + : new CafeThemeCustomColors + { + Primary = dto.Custom.Primary, + Secondary = dto.Custom.Secondary, + Accent = dto.Custom.Accent, + Background = dto.Custom.Background, + Surface = dto.Custom.Surface, + Text = dto.Custom.Text, + TextMuted = dto.Custom.TextMuted, + Destructive = dto.Custom.Destructive, + Success = dto.Custom.Success, + PrimaryOpacity = dto.Custom.PrimaryOpacity, + SecondaryOpacity = dto.Custom.SecondaryOpacity, + AccentOpacity = dto.Custom.AccentOpacity, + BackgroundOpacity = dto.Custom.BackgroundOpacity, + SurfaceOpacity = dto.Custom.SurfaceOpacity, + TextOpacity = dto.Custom.TextOpacity, + TextMutedOpacity = dto.Custom.TextMutedOpacity, + DestructiveOpacity = dto.Custom.DestructiveOpacity, + SuccessOpacity = dto.Custom.SuccessOpacity + } + }; + return CafeThemeSerializer.Normalize(theme); + } + + public static CafeThemeDto FromJson(string? json) => ToDto(CafeThemeSerializer.Parse(json)); +} diff --git a/src/Meezi.API/Services/CoffeeAdvisorService.cs b/src/Meezi.API/Services/CoffeeAdvisorService.cs new file mode 100644 index 0000000..7b648d7 --- /dev/null +++ b/src/Meezi.API/Services/CoffeeAdvisorService.cs @@ -0,0 +1,130 @@ +using System.Text.Json; +using Microsoft.EntityFrameworkCore; +using Meezi.API.Models.Public; +using Meezi.Infrastructure.Data; + +namespace Meezi.API.Services; + +public interface ICoffeeAdvisorService +{ + Task<(CoffeeAdvisorResultDto? Data, string? ErrorCode, string? Message)> RecommendAsync( + CoffeeAdvisorRequest request, + CancellationToken cancellationToken = default); +} + +public class CoffeeAdvisorService : ICoffeeAdvisorService +{ + private static readonly JsonSerializerOptions JsonOpts = new() { PropertyNameCaseInsensitive = true }; + + private readonly AppDbContext _db; + private readonly IOpenAiChatService _openAi; + private readonly ILogger _logger; + + public CoffeeAdvisorService( + AppDbContext db, + IOpenAiChatService openAi, + ILogger logger) + { + _db = db; + _openAi = openAi; + _logger = logger; + } + + public async Task<(CoffeeAdvisorResultDto? Data, string? ErrorCode, string? Message)> RecommendAsync( + CoffeeAdvisorRequest request, + CancellationToken cancellationToken = default) + { + var purpose = request.Purpose?.Trim(); + if (string.IsNullOrWhiteSpace(purpose) || purpose.Length < 3) + return (null, "INVALID_REQUEST", "Describe what you need (at least 3 characters)."); + + if (!await _openAi.IsConfiguredForCoffeeAdvisorAsync(cancellationToken)) + return (null, "AI_NOT_CONFIGURED", "Coffee advisor is not available right now."); + + var menuLines = await LoadMenuContextAsync(request.CafeSlug, cancellationToken); + var systemPrompt = + """ + You are a specialty coffee advisor for Iranian cafés. Respond ONLY with valid JSON (no markdown). + Schema: { "summary": string (1-2 sentences in Persian), "picks": [ { "name": string, "reason": string (Persian), "menuItemId": string|null } ] } + Rules: suggest 1-3 drinks; prefer items from the menu list when provided; match the guest's purpose (energy, relax, meeting, dessert pairing, etc.); be concise and friendly in Persian. + """; + var userPrompt = $""" + Guest purpose: {purpose} + {(menuLines.Count > 0 ? "Café menu (id | name | description):\n" + string.Join("\n", menuLines) : "No specific café menu — suggest classic café drinks.")} + """; + + string? json; + try + { + json = await _openAi.CompleteJsonAsync(systemPrompt, userPrompt, cancellationToken); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Coffee advisor OpenAI call failed"); + return (null, "AI_FAILED", "Could not get a recommendation. Try again later."); + } + + if (string.IsNullOrWhiteSpace(json)) + return (null, "AI_FAILED", "Could not get a recommendation. Try again later."); + + try + { + var parsed = JsonSerializer.Deserialize(json, JsonOpts); + if (parsed is null || string.IsNullOrWhiteSpace(parsed.Summary)) + return (null, "AI_FAILED", "Invalid advisor response."); + + var picks = (parsed.Picks ?? []) + .Where(p => !string.IsNullOrWhiteSpace(p.Name)) + .Take(3) + .Select(p => new CoffeeAdvisorPickDto( + p.Name!.Trim(), + p.Reason?.Trim() ?? "", + string.IsNullOrWhiteSpace(p.MenuItemId) ? null : p.MenuItemId.Trim())) + .ToList(); + + return (new CoffeeAdvisorResultDto(parsed.Summary.Trim(), picks), null, null); + } + catch (JsonException ex) + { + _logger.LogWarning(ex, "Coffee advisor JSON parse failed"); + return (null, "AI_FAILED", "Could not parse advisor response."); + } + } + + private async Task> LoadMenuContextAsync(string? slug, CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(slug)) + return []; + + var cafeId = await _db.Cafes.AsNoTracking() + .Where(c => c.Slug == slug.Trim() && c.DeletedAt == null) + .Select(c => c.Id) + .FirstOrDefaultAsync(cancellationToken); + if (cafeId is null) + return []; + + var items = await _db.MenuItems.AsNoTracking() + .Where(i => i.CafeId == cafeId && i.IsAvailable && i.DeletedAt == null) + .OrderBy(i => i.Name) + .Take(40) + .Select(i => new { i.Id, i.Name, i.Description }) + .ToListAsync(cancellationToken); + + return items + .Select(i => $"{i.Id} | {i.Name} | {(string.IsNullOrWhiteSpace(i.Description) ? "-" : i.Description)}") + .ToList(); + } + + private sealed class AdvisorJson + { + public string? Summary { get; set; } + public List? Picks { get; set; } + } + + private sealed class AdvisorPickJson + { + public string? Name { get; set; } + public string? Reason { get; set; } + public string? MenuItemId { get; set; } + } +} diff --git a/src/Meezi.API/Services/ConsumerAuthService.cs b/src/Meezi.API/Services/ConsumerAuthService.cs new file mode 100644 index 0000000..2a63421 --- /dev/null +++ b/src/Meezi.API/Services/ConsumerAuthService.cs @@ -0,0 +1,193 @@ +using Meezi.API.Models.Auth; +using Meezi.API.Models.Consumer; +using Meezi.API.Security; +using Meezi.Core.Constants; +using Meezi.Core.Entities; +using Meezi.Core.Interfaces; +using Meezi.Core.Utilities; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using StackExchange.Redis; + +namespace Meezi.API.Services; + +public interface IConsumerAuthService +{ + Task<(bool Success, SendOtpResponse? Data, string? ErrorCode, string? ErrorMessage)> SendOtpAsync( + SendOtpRequest request, + CancellationToken cancellationToken = default); + + Task<(bool Success, ConsumerAuthTokenResponse? Data, string? ErrorCode, string? ErrorMessage)> VerifyOtpAsync( + VerifyOtpRequest request, + CancellationToken cancellationToken = default); + + Task<(bool Success, ConsumerAuthTokenResponse? Data, string? ErrorCode, string? ErrorMessage)> RefreshAsync( + RefreshTokenRequest request, + CancellationToken cancellationToken = default); +} + +public class ConsumerAuthService : IConsumerAuthService +{ + private const int OtpTtlSeconds = 300; + private const int DefaultMaxOtpAttemptsPerHour = 5; + + private readonly AppDbContext _db; + private readonly IConnectionMultiplexer _redis; + private readonly ISmsService _smsService; + private readonly IJwtTokenService _jwtTokenService; + private readonly IRefreshTokenStore _refreshTokenStore; + private readonly IConfiguration _configuration; + private readonly ILogger _logger; + private readonly IAbuseProtectionService _abuse; + private readonly IHttpContextAccessor _http; + + public ConsumerAuthService( + AppDbContext db, + IConnectionMultiplexer redis, + ISmsService smsService, + IJwtTokenService jwtTokenService, + IRefreshTokenStore refreshTokenStore, + IConfiguration configuration, + IAbuseProtectionService abuse, + IHttpContextAccessor http, + ILogger logger) + { + _db = db; + _redis = redis; + _smsService = smsService; + _jwtTokenService = jwtTokenService; + _refreshTokenStore = refreshTokenStore; + _configuration = configuration; + _abuse = abuse; + _http = http; + _logger = logger; + } + + public async Task<(bool Success, SendOtpResponse? Data, string? ErrorCode, string? ErrorMessage)> SendOtpAsync( + SendOtpRequest request, + CancellationToken cancellationToken = default) + { + var phone = PhoneNormalizer.Normalize(request.Phone); + var redis = _redis.GetDatabase(); + var maxAttempts = _configuration.GetValue("Auth:MaxOtpAttemptsPerHour", DefaultMaxOtpAttemptsPerHour); + + if (_http.HttpContext is not null) + { + var ip = ClientIpResolver.GetClientIp(_http.HttpContext); + var ipCheck = await _abuse.CheckAuthOtpByIpAsync(ip, cancellationToken); + if (!ipCheck.Allowed) + return (false, null, ipCheck.ErrorCode, ipCheck.Message); + } + + var attemptsKey = $"consumer-otp:attempts:{phone}"; + if (maxAttempts > 0) + { + var attempts = await redis.StringGetAsync(attemptsKey); + if (attempts.HasValue && (int)attempts >= maxAttempts) + return (false, null, "RATE_LIMITED", "Too many OTP requests. Try again later."); + } + + var otp = Random.Shared.Next(100000, 999999).ToString(); + await redis.StringSetAsync($"consumer-otp:{phone}", otp, TimeSpan.FromSeconds(OtpTtlSeconds)); + + if (string.IsNullOrWhiteSpace(_configuration["Kavenegar:ApiKey"])) + _logger.LogWarning("DEV consumer OTP for {Phone}: {Otp}", phone, otp); + + try + { + await _smsService.SendOtpAsync(phone, otp, cancellationToken); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to send consumer OTP SMS"); + return (false, null, "SMS_FAILED", "Could not send verification code."); + } + + if (maxAttempts > 0) + { + var newAttempts = await redis.StringIncrementAsync(attemptsKey); + if (newAttempts == 1) + await redis.KeyExpireAsync(attemptsKey, TimeSpan.FromHours(1)); + } + + return (true, new SendOtpResponse(true, OtpTtlSeconds), null, null); + } + + public async Task<(bool Success, ConsumerAuthTokenResponse? Data, string? ErrorCode, string? ErrorMessage)> VerifyOtpAsync( + VerifyOtpRequest request, + CancellationToken cancellationToken = default) + { + var phone = PhoneNormalizer.Normalize(request.Phone); + var code = OtpNormalizer.Normalize(request.Code); + if (!OtpNormalizer.IsValidSixDigitCode(code)) + return (false, null, "INVALID_OTP", "Invalid or expired verification code."); + + var redis = _redis.GetDatabase(); + var storedOtp = await redis.StringGetAsync($"consumer-otp:{phone}"); + if (storedOtp.IsNullOrEmpty || storedOtp.ToString() != code) + return (false, null, "INVALID_OTP", "Invalid or expired verification code."); + + var account = await _db.ConsumerAccounts + .FirstOrDefaultAsync(a => a.Phone == phone, cancellationToken); + + if (account is null) + { + account = new ConsumerAccount { Phone = phone }; + _db.ConsumerAccounts.Add(account); + await _db.SaveChangesAsync(cancellationToken); + } + + await redis.KeyDeleteAsync($"consumer-otp:{phone}"); + + var tokens = await IssueTokensAsync(account, cancellationToken); + return (true, tokens, null, null); + } + + public async Task<(bool Success, ConsumerAuthTokenResponse? Data, string? ErrorCode, string? ErrorMessage)> RefreshAsync( + RefreshTokenRequest request, + CancellationToken cancellationToken = default) + { + var payload = await _refreshTokenStore.GetAsync(request.RefreshToken, cancellationToken); + if (payload is null || payload.Actor != MeeziActorKinds.Consumer) + return (false, null, "INVALID_TOKEN", "Refresh token is invalid or expired."); + + var account = await _db.ConsumerAccounts + .FirstOrDefaultAsync(a => a.Id == payload.UserId, cancellationToken); + if (account is null) + return (false, null, "NOT_FOUND", "Account no longer exists."); + + await _refreshTokenStore.RevokeAsync(request.RefreshToken, cancellationToken); + var tokens = await IssueTokensAsync(account, cancellationToken); + return (true, tokens, null, null); + } + + private async Task IssueTokensAsync( + ConsumerAccount account, + CancellationToken cancellationToken) + { + var lang = "fa"; + var accessToken = _jwtTokenService.CreateConsumerAccessToken(account, lang); + var refreshToken = _jwtTokenService.CreateRefreshToken(); + var refreshDays = _configuration.GetValue("Jwt:RefreshTokenExpiryDays", 30); + + await _refreshTokenStore.StoreAsync( + refreshToken, + new RefreshTokenPayload( + account.Id, + string.Empty, + MeeziRoles.Customer, + string.Empty, + lang, + MeeziActorKinds.Consumer), + TimeSpan.FromDays(refreshDays), + cancellationToken); + + return new ConsumerAuthTokenResponse( + accessToken, + refreshToken, + _jwtTokenService.GetAccessTokenExpiry(), + account.Id, + account.Phone, + lang); + } +} diff --git a/src/Meezi.API/Services/ConsumerOrdersService.cs b/src/Meezi.API/Services/ConsumerOrdersService.cs new file mode 100644 index 0000000..4f42904 --- /dev/null +++ b/src/Meezi.API/Services/ConsumerOrdersService.cs @@ -0,0 +1,60 @@ +using Meezi.API.Models.Consumer; +using Meezi.API.Services.Printing; +using Meezi.Core.Enums; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; + +namespace Meezi.API.Services; + +public interface IConsumerOrdersService +{ + Task> GetMyOrdersAsync( + string phone, + int page, + int pageSize, + CancellationToken cancellationToken = default); +} + +public class ConsumerOrdersService : IConsumerOrdersService +{ + private readonly AppDbContext _db; + + public ConsumerOrdersService(AppDbContext db) => _db = db; + + public async Task> GetMyOrdersAsync( + string phone, + int page, + int pageSize, + CancellationToken cancellationToken = default) + { + page = Math.Max(1, page); + pageSize = Math.Clamp(pageSize, 1, 50); + + var orders = await _db.Orders + .AsNoTracking() + .Include(o => o.Cafe) + .Include(o => o.Table) + .Include(o => o.Customer) + .Include(o => o.Items) + .Where(o => + o.DeletedAt == null + && (o.GuestPhone == phone + || (o.Customer != null && o.Customer.Phone == phone))) + .OrderByDescending(o => o.CreatedAt) + .Skip((page - 1) * pageSize) + .Take(pageSize) + .ToListAsync(cancellationToken); + + return orders.Select(o => new ConsumerOrderHistoryDto( + o.Id, + o.CafeId, + o.Cafe.Name, + o.Cafe.Slug, + o.Status, + o.Total, + o.DisplayNumber > 0 ? o.DisplayNumber : ReceiptPrintFormatting.StableDisplayNumberFromId(o.Id), + o.CreatedAt, + o.Table?.Number, + o.Items.Count(i => !i.IsVoided))).ToList(); + } +} diff --git a/src/Meezi.API/Services/CouponService.cs b/src/Meezi.API/Services/CouponService.cs new file mode 100644 index 0000000..e4674b8 --- /dev/null +++ b/src/Meezi.API/Services/CouponService.cs @@ -0,0 +1,188 @@ +using Meezi.API.Models.Crm; +using Meezi.Core.Entities; +using Meezi.Core.Enums; +using Meezi.Infrastructure.Data; +using Meezi.Shared; +using Microsoft.EntityFrameworkCore; + +namespace Meezi.API.Services; + +public interface ICouponService +{ + Task> GetAllAsync(string cafeId, CancellationToken cancellationToken = default); + Task GetAsync(string cafeId, string id, CancellationToken cancellationToken = default); + Task CreateAsync(string cafeId, CreateCouponRequest request, CancellationToken cancellationToken = default); + Task UpdateAsync(string cafeId, string id, UpdateCouponRequest request, CancellationToken cancellationToken = default); + Task DeleteAsync(string cafeId, string id, CancellationToken cancellationToken = default); + Task<(ValidateCouponResult? Data, ApiError? Error)> ValidateAsync( + string cafeId, + ValidateCouponRequest request, + CancellationToken cancellationToken = default); +} + +public class CouponService : ICouponService +{ + private readonly AppDbContext _db; + + public CouponService(AppDbContext db) + { + _db = db; + } + + public async Task> GetAllAsync(string cafeId, CancellationToken cancellationToken = default) + { + var list = await _db.Coupons + .Where(c => c.CafeId == cafeId) + .OrderByDescending(c => c.CreatedAt) + .ToListAsync(cancellationToken); + return list.Select(ToDto).ToList(); + } + + public async Task GetAsync(string cafeId, string id, CancellationToken cancellationToken = default) + { + var entity = await _db.Coupons + .FirstOrDefaultAsync(c => c.Id == id && c.CafeId == cafeId, cancellationToken); + return entity is null ? null : ToDto(entity); + } + + public async Task CreateAsync( + string cafeId, + CreateCouponRequest request, + CancellationToken cancellationToken = default) + { + var codeExists = await _db.Coupons.AnyAsync( + c => c.CafeId == cafeId && c.Code == request.Code.ToUpperInvariant(), cancellationToken); + if (codeExists) return null; + + var entity = new Coupon + { + CafeId = cafeId, + Code = request.Code.ToUpperInvariant(), + Type = request.Type, + Value = request.Value, + MinOrderAmount = request.MinOrderAmount, + MaxDiscount = request.MaxDiscount, + UsageLimit = request.UsageLimit, + TargetGroup = request.TargetGroup, + StartsAt = request.StartsAt, + ExpiresAt = request.ExpiresAt, + IsActive = request.IsActive + }; + + _db.Coupons.Add(entity); + await _db.SaveChangesAsync(cancellationToken); + return ToDto(entity); + } + + public async Task UpdateAsync( + string cafeId, + string id, + UpdateCouponRequest request, + CancellationToken cancellationToken = default) + { + var entity = await _db.Coupons + .FirstOrDefaultAsync(c => c.Id == id && c.CafeId == cafeId, cancellationToken); + if (entity is null) return null; + + if (request.Code is not null) entity.Code = request.Code.ToUpperInvariant(); + if (request.Type.HasValue) entity.Type = request.Type.Value; + if (request.Value.HasValue) entity.Value = request.Value.Value; + if (request.MinOrderAmount.HasValue) entity.MinOrderAmount = request.MinOrderAmount; + if (request.MaxDiscount.HasValue) entity.MaxDiscount = request.MaxDiscount; + if (request.UsageLimit.HasValue) entity.UsageLimit = request.UsageLimit; + if (request.TargetGroup.HasValue) entity.TargetGroup = request.TargetGroup; + if (request.StartsAt.HasValue) entity.StartsAt = request.StartsAt; + if (request.ExpiresAt.HasValue) entity.ExpiresAt = request.ExpiresAt; + if (request.IsActive.HasValue) entity.IsActive = request.IsActive.Value; + + await _db.SaveChangesAsync(cancellationToken); + return ToDto(entity); + } + + public async Task DeleteAsync(string cafeId, string id, CancellationToken cancellationToken = default) + { + var entity = await _db.Coupons + .FirstOrDefaultAsync(c => c.Id == id && c.CafeId == cafeId, cancellationToken); + if (entity is null) return false; + + entity.DeletedAt = DateTime.UtcNow; + entity.IsActive = false; + await _db.SaveChangesAsync(cancellationToken); + return true; + } + + public async Task<(ValidateCouponResult? Data, ApiError? Error)> ValidateAsync( + string cafeId, + ValidateCouponRequest request, + CancellationToken cancellationToken = default) + { + var code = request.Code.Trim().ToUpperInvariant(); + if (string.IsNullOrEmpty(code)) + return (null, new ApiError("COUPON_REQUIRED", "Coupon code is required.")); + + if (request.Subtotal <= 0) + return (null, new ApiError("CART_EMPTY", "Add items before applying a coupon.")); + + var coupon = await _db.Coupons.FirstOrDefaultAsync( + c => c.CafeId == cafeId && c.Code == code, + cancellationToken); + + if (coupon is null) + return (null, new ApiError("COUPON_NOT_FOUND", "Coupon code is invalid.")); + + if (!coupon.IsActive || coupon.DeletedAt is not null) + return (null, new ApiError("COUPON_INACTIVE", "This coupon is not active.")); + + var now = DateTime.UtcNow; + if (coupon.StartsAt is not null && coupon.StartsAt > now) + return (null, new ApiError("COUPON_NOT_STARTED", "This coupon is not valid yet.")); + + if (coupon.ExpiresAt is not null && coupon.ExpiresAt < now) + return (null, new ApiError("COUPON_EXPIRED", "This coupon has expired.")); + + if (coupon.UsageLimit is not null && coupon.UsedCount >= coupon.UsageLimit) + return (null, new ApiError("COUPON_LIMIT_REACHED", "This coupon has reached its usage limit.")); + + if (coupon.MinOrderAmount is not null && request.Subtotal < coupon.MinOrderAmount) + return (null, new ApiError( + "COUPON_MIN_ORDER", + $"Minimum order amount is {coupon.MinOrderAmount:N0} T.")); + + var discount = CalculateDiscount(coupon, request.Subtotal); + if (discount <= 0) + return (null, new ApiError("COUPON_NO_DISCOUNT", "This coupon does not apply to this order.")); + + return (new ValidateCouponResult( + coupon.Id, + coupon.Code, + coupon.Type, + coupon.Value, + discount), null); + } + + internal static decimal CalculateDiscount(Coupon coupon, decimal subtotal) + { + return coupon.Type switch + { + CouponType.Percentage => Math.Min( + Math.Round(subtotal * coupon.Value / 100m, 0), + coupon.MaxDiscount ?? subtotal), + CouponType.FixedAmount => Math.Min(coupon.Value, subtotal), + _ => 0m + }; + } + + private static CouponDto ToDto(Coupon c) => new( + c.Id, + c.Code, + c.Type, + c.Value, + c.MinOrderAmount, + c.MaxDiscount, + c.UsageLimit, + c.UsedCount, + c.TargetGroup, + c.StartsAt, + c.ExpiresAt, + c.IsActive); +} diff --git a/src/Meezi.API/Services/CustomerService.cs b/src/Meezi.API/Services/CustomerService.cs new file mode 100644 index 0000000..7515e8f --- /dev/null +++ b/src/Meezi.API/Services/CustomerService.cs @@ -0,0 +1,136 @@ +using Meezi.API.Models.Crm; +using Meezi.Core.Entities; +using Meezi.Core.Utilities; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; + +namespace Meezi.API.Services; + +public interface ICustomerService +{ + Task> SearchAsync(string cafeId, string? query, CancellationToken cancellationToken = default); + Task GetAsync(string cafeId, string id, CancellationToken cancellationToken = default); + Task CreateAsync(string cafeId, CreateCustomerRequest request, CancellationToken cancellationToken = default); + Task UpdateAsync(string cafeId, string id, UpdateCustomerRequest request, CancellationToken cancellationToken = default); + Task DeleteAsync(string cafeId, string id, CancellationToken cancellationToken = default); +} + +public class CustomerService : ICustomerService +{ + private readonly AppDbContext _db; + + public CustomerService(AppDbContext db) + { + _db = db; + } + + public async Task> SearchAsync( + string cafeId, + string? query, + CancellationToken cancellationToken = default) + { + var q = _db.Customers.Where(c => c.CafeId == cafeId && c.DeletedAt == null); + + if (!string.IsNullOrWhiteSpace(query)) + { + var term = query.Trim(); + var normalizedPhone = PhoneNormalizer.Normalize(term); + q = q.Where(c => + c.Name.Contains(term) || + c.Phone.Contains(term) || + (c.NationalId != null && c.NationalId.Contains(term)) || + (normalizedPhone.Length >= 10 && c.Phone.Contains(normalizedPhone))); + } + + var list = await q + .OrderByDescending(c => c.CreatedAt) + .Take(100) + .ToListAsync(cancellationToken); + return list.Select(ToDto).ToList(); + } + + public async Task GetAsync(string cafeId, string id, CancellationToken cancellationToken = default) + { + var entity = await _db.Customers + .FirstOrDefaultAsync(c => c.Id == id && c.CafeId == cafeId && c.DeletedAt == null, cancellationToken); + return entity is null ? null : ToDto(entity); + } + + public async Task CreateAsync( + string cafeId, + CreateCustomerRequest request, + CancellationToken cancellationToken = default) + { + var phone = PhoneNormalizer.Normalize(request.Phone); + var exists = await _db.Customers.AnyAsync( + c => c.CafeId == cafeId && c.Phone == phone, cancellationToken); + if (exists) return null; + + var entity = new Customer + { + CafeId = cafeId, + Name = request.Name, + Phone = phone, + NationalId = request.NationalId, + BirthDateJalali = request.BirthDateJalali, + Group = request.Group, + ReferredBy = request.ReferredBy + }; + + _db.Customers.Add(entity); + await _db.SaveChangesAsync(cancellationToken); + return ToDto(entity); + } + + public async Task UpdateAsync( + string cafeId, + string id, + UpdateCustomerRequest request, + CancellationToken cancellationToken = default) + { + var entity = await _db.Customers + .FirstOrDefaultAsync(c => c.Id == id && c.CafeId == cafeId && c.DeletedAt == null, cancellationToken); + if (entity is null) return null; + + if (request.Name is not null) entity.Name = request.Name; + if (request.Phone is not null) + { + var phone = PhoneNormalizer.Normalize(request.Phone); + var phoneTaken = await _db.Customers.AnyAsync( + c => c.CafeId == cafeId && c.Phone == phone && c.Id != id && c.DeletedAt == null, + cancellationToken); + if (phoneTaken) return null; + entity.Phone = phone; + } + if (request.NationalId is not null) entity.NationalId = request.NationalId; + if (request.BirthDateJalali is not null) entity.BirthDateJalali = request.BirthDateJalali; + if (request.Group.HasValue) entity.Group = request.Group.Value; + if (request.LoyaltyPoints.HasValue) entity.LoyaltyPoints = request.LoyaltyPoints.Value; + if (request.ReferredBy is not null) entity.ReferredBy = request.ReferredBy; + + await _db.SaveChangesAsync(cancellationToken); + return ToDto(entity); + } + + public async Task DeleteAsync(string cafeId, string id, CancellationToken cancellationToken = default) + { + var entity = await _db.Customers + .FirstOrDefaultAsync(c => c.Id == id && c.CafeId == cafeId, cancellationToken); + if (entity is null) return false; + + entity.DeletedAt = DateTime.UtcNow; + await _db.SaveChangesAsync(cancellationToken); + return true; + } + + private static CustomerDto ToDto(Customer c) => new( + c.Id, + c.Name, + c.Phone, + c.NationalId, + c.BirthDateJalali, + c.Group, + c.LoyaltyPoints, + c.ReferredBy, + c.CreatedAt); +} diff --git a/src/Meezi.API/Services/DailyReportService.cs b/src/Meezi.API/Services/DailyReportService.cs new file mode 100644 index 0000000..25ddacd --- /dev/null +++ b/src/Meezi.API/Services/DailyReportService.cs @@ -0,0 +1,306 @@ +using Meezi.API.Models.Reports; +using Meezi.Core.Entities; +using Meezi.Core.Enums; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; + +namespace Meezi.API.Services; + +public interface IDailyReportService +{ + Task GenerateReportAsync( + string cafeId, + string branchId, + DateOnly date, + CancellationToken cancellationToken = default); + + Task GetReportAsync( + string cafeId, + string branchId, + DateOnly date, + CancellationToken cancellationToken = default); + + Task> GetReportRangeAsync( + string cafeId, + string? branchId, + DateOnly startDate, + DateOnly endDate, + CancellationToken cancellationToken = default); + + Task GetSummaryAsync( + string cafeId, + int days, + CancellationToken cancellationToken = default); +} + +public class DailyReportService : IDailyReportService +{ + private static readonly OrderStatus ClosedOrderStatus = OrderStatus.Delivered; + + private readonly AppDbContext _db; + private readonly ILogger _logger; + + public DailyReportService(AppDbContext db, ILogger logger) + { + _db = db; + _logger = logger; + } + + public async Task GenerateReportAsync( + string cafeId, + string branchId, + DateOnly date, + CancellationToken cancellationToken = default) + { + await EnsureBranchAsync(cafeId, branchId, cancellationToken); + + var (utcStart, utcEnd) = IranCalendar.GetUtcRangeForIranDay(date); + var metrics = await ComputeMetricsAsync(cafeId, branchId, utcStart, utcEnd, cancellationToken); + + var existing = await _db.DailyReports.FirstOrDefaultAsync( + r => r.CafeId == cafeId && r.BranchId == branchId && r.Date == date, + cancellationToken); + + var now = DateTime.UtcNow; + if (existing is null) + { + existing = new DailyReport + { + CafeId = cafeId, + BranchId = branchId, + Date = date, + CreatedAt = now + }; + _db.DailyReports.Add(existing); + } + + ApplyMetrics(existing, metrics, now); + await _db.SaveChangesAsync(cancellationToken); + + _logger.LogInformation( + "Daily report generated for cafe {CafeId} branch {BranchId} date {Date}", + cafeId, branchId, date); + + return ToDto(existing); + } + + public async Task GetReportAsync( + string cafeId, + string branchId, + DateOnly date, + CancellationToken cancellationToken = default) + { + var row = await _db.DailyReports.AsNoTracking() + .FirstOrDefaultAsync( + r => r.CafeId == cafeId && r.BranchId == branchId && r.Date == date, + cancellationToken); + + return row is null ? null : ToDto(row); + } + + public async Task> GetReportRangeAsync( + string cafeId, + string? branchId, + DateOnly startDate, + DateOnly endDate, + CancellationToken cancellationToken = default) + { + var query = _db.DailyReports.AsNoTracking() + .Where(r => r.CafeId == cafeId && r.Date >= startDate && r.Date <= endDate); + + if (!string.IsNullOrEmpty(branchId)) + query = query.Where(r => r.BranchId == branchId); + + var rows = await query.OrderBy(r => r.Date).ThenBy(r => r.BranchId).ToListAsync(cancellationToken); + return rows.Select(ToDto).ToList(); + } + + public async Task GetSummaryAsync( + string cafeId, + int days, + CancellationToken cancellationToken = default) + { + days = Math.Clamp(days, 1, 365); + var today = IranCalendar.TodayInIran; + var from = today.AddDays(-(days - 1)); + + var rows = await GetReportRangeAsync(cafeId, null, from, today, cancellationToken); + + if (rows.Count == 0) + { + return new DailyReportSummaryDto( + days, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, []); + } + + return new DailyReportSummaryDto( + days, + rows.Sum(r => r.TotalRevenue), + rows.Sum(r => r.CashRevenue), + rows.Sum(r => r.CardRevenue), + rows.Sum(r => r.CreditRevenue), + rows.Sum(r => r.TotalOrders), + rows.Sum(r => r.TotalOrders) > 0 + ? rows.Sum(r => r.TotalRevenue) / rows.Sum(r => r.TotalOrders) + : 0, + rows.Sum(r => r.TotalVoids), + rows.Sum(r => r.VoidAmount), + rows.Sum(r => r.TotalExpenses), + rows.Sum(r => r.NetIncome), + rows); + } + + private async Task EnsureBranchAsync(string cafeId, string branchId, CancellationToken ct) + { + var exists = await _db.Branches.AnyAsync( + b => b.Id == branchId && b.CafeId == cafeId && b.IsActive, ct); + if (!exists) + throw new InvalidOperationException("Branch not found."); + } + + private async Task ComputeMetricsAsync( + string cafeId, + string branchId, + DateTime utcStart, + DateTime utcEnd, + CancellationToken cancellationToken) + { + var closedOrders = await _db.Orders + .Where(o => o.CafeId == cafeId + && o.BranchId == branchId + && o.Status == ClosedOrderStatus + && o.CreatedAt >= utcStart + && o.CreatedAt < utcEnd) + .Select(o => o.Id) + .ToListAsync(cancellationToken); + + var orderItems = await _db.OrderItems + .Include(i => i.MenuItem) + .Include(i => i.Order) + .Where(i => i.Order.CafeId == cafeId + && i.Order.BranchId == branchId + && i.Order.Status == ClosedOrderStatus + && i.Order.CreatedAt >= utcStart + && i.Order.CreatedAt < utcEnd) + .ToListAsync(cancellationToken); + + var activeLines = orderItems.Where(i => !i.IsVoided).ToList(); + var voidedLines = orderItems.Where(i => i.IsVoided).ToList(); + + var totalRevenue = activeLines.Sum(i => i.UnitPrice * i.Quantity); + var totalOrders = closedOrders.Count; + var avgOrderValue = totalOrders > 0 ? totalRevenue / totalOrders : 0; + + var cashTx = await _db.CashTransactions + .Where(t => t.CafeId == cafeId + && t.BranchId == branchId + && t.CreatedAt >= utcStart + && t.CreatedAt < utcEnd) + .ToListAsync(cancellationToken); + + var payments = cashTx.Where(t => t.Type == CashTransactionType.OrderPayment).ToList(); + var cashRevenue = payments.Where(t => t.Method == PaymentMethod.Cash).Sum(t => t.Amount); + var cardRevenue = payments.Where(t => t.Method == PaymentMethod.Card).Sum(t => t.Amount); + var creditRevenue = payments.Where(t => t.Method == PaymentMethod.Credit).Sum(t => t.Amount); + + if (payments.Count == 0 && closedOrders.Count > 0) + { + var orderPayments = await _db.Payments + .Include(p => p.Order) + .Where(p => p.Order.CafeId == cafeId + && p.Order.BranchId == branchId + && p.Order.Status == ClosedOrderStatus + && p.Status == PaymentStatus.Completed + && p.Order.CreatedAt >= utcStart + && p.Order.CreatedAt < utcEnd) + .ToListAsync(cancellationToken); + + cashRevenue = orderPayments.Where(p => p.Method == PaymentMethod.Cash).Sum(p => p.Amount); + cardRevenue = orderPayments.Where(p => p.Method == PaymentMethod.Card).Sum(p => p.Amount); + creditRevenue = orderPayments.Where(p => p.Method == PaymentMethod.Credit).Sum(p => p.Amount); + } + + var totalExpenses = await _db.Expenses + .Where(e => e.CafeId == cafeId + && e.BranchId == branchId + && e.CreatedAt >= utcStart + && e.CreatedAt < utcEnd) + .SumAsync(e => e.Amount, cancellationToken); + + var voidAmount = voidedLines.Sum(i => i.UnitPrice * i.Quantity); + var netIncome = totalRevenue - totalExpenses - voidAmount; + + var topProducts = activeLines + .GroupBy(i => i.MenuItemId) + .Select(g => new TopProductEntry + { + ProductId = g.Key, + Name = g.First().MenuItem.Name, + Quantity = g.Sum(x => x.Quantity), + Revenue = g.Sum(x => x.UnitPrice * x.Quantity) + }) + .OrderByDescending(x => x.Revenue) + .Take(10) + .ToList(); + + return new DailyMetrics( + totalRevenue, + cashRevenue, + cardRevenue, + creditRevenue, + totalOrders, + avgOrderValue, + voidedLines.Count, + voidAmount, + totalExpenses, + netIncome, + topProducts); + } + + private static void ApplyMetrics(DailyReport entity, DailyMetrics metrics, DateTime generatedAt) + { + entity.TotalRevenue = metrics.TotalRevenue; + entity.CashRevenue = metrics.CashRevenue; + entity.CardRevenue = metrics.CardRevenue; + entity.CreditRevenue = metrics.CreditRevenue; + entity.TotalOrders = metrics.TotalOrders; + entity.AvgOrderValue = metrics.AvgOrderValue; + entity.TotalVoids = metrics.TotalVoids; + entity.VoidAmount = metrics.VoidAmount; + entity.TotalExpenses = metrics.TotalExpenses; + entity.NetIncome = metrics.NetIncome; + entity.TopProducts = metrics.TopProducts; + entity.GeneratedAt = generatedAt; + } + + private static DailyReportSnapshotDto ToDto(DailyReport r) => new( + r.Id, + r.CafeId, + r.BranchId, + r.Date.ToString("yyyy-MM-dd"), + r.TotalRevenue, + r.CashRevenue, + r.CardRevenue, + r.CreditRevenue, + r.TotalOrders, + r.AvgOrderValue, + r.TotalVoids, + r.VoidAmount, + r.TotalExpenses, + r.NetIncome, + r.TopProducts.Select(p => new TopProductSnapshotDto( + p.ProductId, p.Name, p.Quantity, p.Revenue)).ToList(), + r.GeneratedAt); + + private sealed record DailyMetrics( + decimal TotalRevenue, + decimal CashRevenue, + decimal CardRevenue, + decimal CreditRevenue, + int TotalOrders, + decimal AvgOrderValue, + int TotalVoids, + decimal VoidAmount, + decimal TotalExpenses, + decimal NetIncome, + List TopProducts); +} diff --git a/src/Meezi.API/Services/Delivery/CommissionCalculator.cs b/src/Meezi.API/Services/Delivery/CommissionCalculator.cs new file mode 100644 index 0000000..3f9cb7d --- /dev/null +++ b/src/Meezi.API/Services/Delivery/CommissionCalculator.cs @@ -0,0 +1,72 @@ +using Meezi.API.Configuration; +using Meezi.Core.Delivery; +using Meezi.Core.Enums; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; + +namespace Meezi.API.Services.Delivery; + +public interface ICommissionCalculator +{ + Task ResolveRatePercentAsync(string cafeId, DeliveryPlatform platform, CancellationToken ct = default); + decimal CalculateCommission(decimal grossTotal, decimal ratePercent); + Task CalculateForOrderAsync( + string cafeId, + UnifiedDeliveryOrder order, + CancellationToken ct = default); +} + +public class CommissionCalculator : ICommissionCalculator +{ + private readonly AppDbContext _db; + private readonly DeliveryPlatformsOptions _options; + + public CommissionCalculator(AppDbContext db, IOptions options) + { + _db = db; + _options = options.Value; + } + + public async Task ResolveRatePercentAsync( + string cafeId, + DeliveryPlatform platform, + CancellationToken ct = default) + { + var custom = await _db.DeliveryCommissionRates + .Where(r => r.CafeId == cafeId && r.Platform == platform && r.IsActive) + .Select(r => (decimal?)r.RatePercent) + .FirstOrDefaultAsync(ct); + + if (custom is > 0) + return custom.Value; + + return platform switch + { + DeliveryPlatform.Snappfood => _options.DefaultSnappfoodCommissionPercent, + DeliveryPlatform.Tap30 => _options.DefaultTap30CommissionPercent, + DeliveryPlatform.Digikala => _options.DefaultDigikalaCommissionPercent, + _ => 0m + }; + } + + public decimal CalculateCommission(decimal grossTotal, decimal ratePercent) => + grossTotal <= 0 || ratePercent <= 0 + ? 0m + : Math.Round(grossTotal * ratePercent / 100m, 0); + + public async Task CalculateForOrderAsync( + string cafeId, + UnifiedDeliveryOrder order, + CancellationToken ct = default) + { + if (order.Payment.Commission is decimal fromPlatform) + return fromPlatform; + + var rate = await ResolveRatePercentAsync(cafeId, order.Platform, ct); + var gross = order.Payment.Total > 0 + ? order.Payment.Total + : order.Items.Sum(i => i.UnitPrice * i.Quantity); + return CalculateCommission(gross, rate); + } +} diff --git a/src/Meezi.API/Services/Delivery/DeliveryFinanceReportService.cs b/src/Meezi.API/Services/Delivery/DeliveryFinanceReportService.cs new file mode 100644 index 0000000..d5e727b --- /dev/null +++ b/src/Meezi.API/Services/Delivery/DeliveryFinanceReportService.cs @@ -0,0 +1,80 @@ +using Meezi.API.Models.Delivery; +using Meezi.Core.Enums; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; + +namespace Meezi.API.Services.Delivery; + +public interface IDeliveryFinanceReportService +{ + Task GetRevenueByPlatformAsync( + string cafeId, + DateTime utcFrom, + DateTime utcTo, + CancellationToken ct = default); +} + +public class DeliveryFinanceReportService : IDeliveryFinanceReportService +{ + private static readonly OrderStatus[] RevenueStatuses = + [ + OrderStatus.Confirmed, + OrderStatus.Preparing, + OrderStatus.Ready, + OrderStatus.Delivered + ]; + + private readonly AppDbContext _db; + + public DeliveryFinanceReportService(AppDbContext db) => _db = db; + + public async Task GetRevenueByPlatformAsync( + string cafeId, + DateTime utcFrom, + DateTime utcTo, + CancellationToken ct = default) + { + var orders = await _db.Orders + .Where(o => o.CafeId == cafeId + && o.DeliveryPlatform != null + && o.CreatedAt >= utcFrom + && o.CreatedAt < utcTo + && RevenueStatuses.Contains(o.Status)) + .ToListAsync(ct); + + var platforms = Enum.GetValues() + .Where(p => p != DeliveryPlatform.Direct) + .Select(platform => + { + var subset = orders.Where(o => o.DeliveryPlatform == platform).ToList(); + var gross = subset.Sum(o => o.Total); + var commission = subset.Sum(o => o.PlatformCommission); + return new PlatformRevenueDto( + platform, + PlatformLabel(platform), + subset.Count, + gross, + commission, + gross - commission); + }) + .Where(p => p.OrderCount > 0) + .ToList(); + + return new DeliveryRevenueReportDto( + $"{utcFrom:yyyy-MM-dd} — {utcTo:yyyy-MM-dd}", + utcFrom, + utcTo, + platforms, + platforms.Sum(p => p.GrossRevenue), + platforms.Sum(p => p.Commission), + platforms.Sum(p => p.NetRevenue)); + } + + private static string PlatformLabel(DeliveryPlatform platform) => platform switch + { + DeliveryPlatform.Snappfood => "اسنپ‌فود", + DeliveryPlatform.Tap30 => "تپسی", + DeliveryPlatform.Digikala => "دیجی‌کالا", + _ => platform.ToString() + }; +} diff --git a/src/Meezi.API/Services/Delivery/DeliveryOrderProcessor.cs b/src/Meezi.API/Services/Delivery/DeliveryOrderProcessor.cs new file mode 100644 index 0000000..a4f0228 --- /dev/null +++ b/src/Meezi.API/Services/Delivery/DeliveryOrderProcessor.cs @@ -0,0 +1,349 @@ +using System.Text.Json; +using Meezi.API.Models.Orders; +using Meezi.API.Services.Printing; +using Meezi.Core.Delivery; +using Meezi.Core.Entities; +using Meezi.Core.Enums; +using Meezi.Core.Interfaces; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; + +namespace Meezi.API.Services.Delivery; + +public record DeliveryProcessResult(bool Success, string? MeeziOrderId, string? ErrorCode, string? Message); + +public interface IDeliveryOrderProcessor +{ + Task ProcessAsync( + string webhookLogId, + UnifiedDeliveryOrder unified, + CancellationToken ct = default); +} + +public class DeliveryOrderProcessor : IDeliveryOrderProcessor +{ + private readonly AppDbContext _db; + private readonly IKdsNotifier _kds; + private readonly ICommissionCalculator _commission; + private readonly IInventoryService _inventory; + private readonly ISnappfoodClient _snappfood; + private readonly ITap30Client _tap30; + private readonly IServiceScopeFactory _scopeFactory; + private readonly ILogger _logger; + + public DeliveryOrderProcessor( + AppDbContext db, + IKdsNotifier kds, + ICommissionCalculator commission, + IInventoryService inventory, + ISnappfoodClient snappfood, + ITap30Client tap30, + IServiceScopeFactory scopeFactory, + ILogger logger) + { + _db = db; + _kds = kds; + _commission = commission; + _inventory = inventory; + _snappfood = snappfood; + _tap30 = tap30; + _scopeFactory = scopeFactory; + _logger = logger; + } + + public async Task ProcessAsync( + string webhookLogId, + UnifiedDeliveryOrder unified, + CancellationToken ct = default) + { + var log = await _db.WebhookLogs.FirstOrDefaultAsync(w => w.Id == webhookLogId, ct); + if (log is null) + return new DeliveryProcessResult(false, null, "LOG_NOT_FOUND", "Webhook log missing."); + + log.AttemptCount++; + try + { + var cafe = await ResolveCafeAsync(unified, ct); + if (cafe is null) + { + await FailLogAsync(log, "Unknown vendor.", ct); + return new DeliveryProcessResult(false, null, "VENDOR_NOT_FOUND", "Unknown vendor."); + } + + log.CafeId = cafe.Id; + log.ExternalOrderId = unified.ExternalId; + + var duplicate = await _db.Orders.AnyAsync( + o => o.CafeId == cafe.Id + && o.DeliveryPlatform == unified.Platform + && o.ExternalOrderId == unified.ExternalId, + ct); + + if (duplicate) + { + await CompleteLogAsync(log, null, success: true, error: null, ct); + return new DeliveryProcessResult(true, null, null, "Duplicate ignored."); + } + + var branchId = await _db.Branches + .Where(b => b.CafeId == cafe.Id && b.IsActive) + .OrderBy(b => b.Name) + .Select(b => b.Id) + .FirstOrDefaultAsync(ct); + + var menuItems = await _db.MenuItems + .Where(m => m.CafeId == cafe.Id && m.IsAvailable) + .ToListAsync(ct); + + var orderItems = new List(); + decimal subtotal = 0; + + foreach (var line in unified.Items) + { + var menuItem = menuItems.FirstOrDefault(m => + (!string.IsNullOrEmpty(line.Sku) && m.Id == line.Sku) + || m.Name.Equals(line.Name, StringComparison.OrdinalIgnoreCase) + || (m.NameEn != null && m.NameEn.Equals(line.Name, StringComparison.OrdinalIgnoreCase))); + + if (menuItem is null) + { + _logger.LogWarning( + "Delivery {Platform} item {Name} not matched for cafe {CafeId}", + unified.Platform, + line.Name, + cafe.Id); + continue; + } + + subtotal += line.UnitPrice * line.Quantity; + orderItems.Add(new OrderItem + { + MenuItemId = menuItem.Id, + Quantity = line.Quantity, + UnitPrice = line.UnitPrice, + Notes = line.Notes ?? unified.Platform.ToString() + }); + } + + if (orderItems.Count == 0) + { + await FailLogAsync(log, "No menu items matched.", ct); + return new DeliveryProcessResult(false, null, "INVALID_MENU_ITEMS", "No menu items matched."); + } + + var platformCommission = await _commission.CalculateForOrderAsync(cafe.Id, unified, ct); + + var taxRate = cafe.DefaultTaxRate > 0 + ? cafe.DefaultTaxRate + : await _db.Taxes.Where(t => t.CafeId == cafe.Id && t.IsDefault) + .Select(t => t.Rate) + .FirstOrDefaultAsync(ct); + + if (taxRate == 0) taxRate = 9m; + + var gross = unified.Payment.Total > 0 ? unified.Payment.Total : subtotal; + var taxTotal = Math.Round((gross - platformCommission) * taxRate / 100m, 0); + var total = gross; + + var displayNumber = await AllocateDisplayNumberAsync(cafe.Id, ct); + var order = new Order + { + CafeId = cafe.Id, + BranchId = branchId, + OrderType = unified.Delivery.Type == "pickup" ? OrderType.Takeaway : OrderType.Delivery, + Source = MapSource(unified.Platform), + Status = MapStatus(unified.Status), + DisplayNumber = displayNumber, + ExternalOrderId = unified.ExternalId, + DeliveryPlatform = unified.Platform, + PlatformCommission = platformCommission, + DeliveryMetaJson = JsonSerializer.Serialize(unified.Delivery), + Subtotal = subtotal, + TaxTotal = taxTotal, + Total = total, + Items = orderItems + }; + + if (unified.Platform == DeliveryPlatform.Snappfood) + order.SnappfoodOrderId = unified.ExternalId; + + if (!string.IsNullOrWhiteSpace(unified.Customer.Phone)) + { + var phone = unified.Customer.Phone.Trim(); + var customer = await _db.Customers + .FirstOrDefaultAsync(c => c.CafeId == cafe.Id && c.Phone == phone, ct); + if (customer is null) + { + customer = new Customer + { + CafeId = cafe.Id, + Name = unified.Customer.Name, + Phone = phone, + Group = CustomerGroup.New + }; + _db.Customers.Add(customer); + await _db.SaveChangesAsync(ct); + } + + order.CustomerId = customer.Id; + order.GuestName = unified.Customer.Name; + order.GuestPhone = phone; + } + else + { + order.GuestName = unified.Customer.Name; + } + + if (unified.Payment.IsPaid && total > 0) + { + order.Payments.Add(new Payment + { + Method = unified.Payment.Method.Equals("cash", StringComparison.OrdinalIgnoreCase) + ? PaymentMethod.Cash + : PaymentMethod.Card, + Amount = total, + Status = PaymentStatus.Completed + }); + } + + _db.Orders.Add(order); + await _db.SaveChangesAsync(ct); + + await TryDeductInventoryAsync(cafe.Id, orderItems, ct); + + var loaded = await _db.Orders + .Include(o => o.Items) + .ThenInclude(i => i.MenuItem) + .Include(o => o.Table) + .FirstAsync(o => o.Id == order.Id, ct); + + await _kds.NotifyOrderCreatedAsync(cafe.Id, MapLive(loaded), ct); + PrinterBackgroundJobs.QueueKitchenPrint(_scopeFactory, cafe.Id, order.Id); + + await AcknowledgePlatformAsync(unified, ct); + + await CompleteLogAsync(log, order.Id, success: true, error: null, ct); + return new DeliveryProcessResult(true, order.Id, null, null); + } + catch (Exception ex) + { + _logger.LogError(ex, "Delivery order processing failed for log {LogId}", webhookLogId); + await FailLogAsync(log, ex.Message, ct); + throw; + } + } + + private async Task ResolveCafeAsync(UnifiedDeliveryOrder unified, CancellationToken ct) => + unified.Platform switch + { + DeliveryPlatform.Snappfood => await _db.Cafes + .FirstOrDefaultAsync(c => c.SnappfoodVendorId == unified.VendorId, ct), + DeliveryPlatform.Tap30 => await _db.Cafes + .FirstOrDefaultAsync(c => c.Tap30VendorId == unified.VendorId, ct), + DeliveryPlatform.Digikala => await _db.Cafes + .FirstOrDefaultAsync(c => c.DigikalaVendorId == unified.VendorId, ct), + _ => null + }; + + private async Task AcknowledgePlatformAsync(UnifiedDeliveryOrder unified, CancellationToken ct) + { + switch (unified.Platform) + { + case DeliveryPlatform.Snappfood: + await _snappfood.AcknowledgeOrderAsync(unified.ExternalId, ct); + break; + case DeliveryPlatform.Tap30: + await _tap30.AcknowledgeOrderAsync(unified.ExternalId, ct); + break; + } + } + + private async Task TryDeductInventoryAsync( + string cafeId, + List items, + CancellationToken ct) + { + if (items.Count == 0) return; + var orderId = items[0].OrderId; + await _inventory.DeductForOrderAsync( + cafeId, + orderId, + items.Select(i => (i.MenuItemId, i.Quantity)).ToList(), + ct); + } + + private static OrderSource MapSource(DeliveryPlatform platform) => platform switch + { + DeliveryPlatform.Snappfood => OrderSource.SnappFood, + DeliveryPlatform.Tap30 => OrderSource.Tap30, + DeliveryPlatform.Digikala => OrderSource.Digikala, + _ => OrderSource.Pos + }; + + private static OrderStatus MapStatus(UnifiedDeliveryStatus status) => status switch + { + UnifiedDeliveryStatus.Pending => OrderStatus.Pending, + UnifiedDeliveryStatus.Confirmed => OrderStatus.Confirmed, + UnifiedDeliveryStatus.Preparing => OrderStatus.Preparing, + UnifiedDeliveryStatus.Ready => OrderStatus.Ready, + UnifiedDeliveryStatus.Delivered => OrderStatus.Delivered, + UnifiedDeliveryStatus.Cancelled => OrderStatus.Cancelled, + _ => OrderStatus.Confirmed + }; + + private async Task FailLogAsync(WebhookLog log, string error, CancellationToken ct) + { + log.Success = false; + log.Processed = true; + log.ErrorMessage = error.Length > 2000 ? error[..2000] : error; + log.ProcessedAt = DateTime.UtcNow; + await _db.SaveChangesAsync(ct); + + if (log.AttemptCount >= 3) + _logger.LogError( + "Delivery webhook dead-letter: platform {Platform} external {ExternalId} — {Error}", + log.Platform, + log.ExternalOrderId, + error); + } + + private async Task CompleteLogAsync( + WebhookLog log, + string? meeziOrderId, + bool success, + string? error, + CancellationToken ct) + { + log.Success = success; + log.Processed = true; + log.MeeziOrderId = meeziOrderId; + log.ErrorMessage = error; + log.ProcessedAt = DateTime.UtcNow; + await _db.SaveChangesAsync(ct); + } + + + private async Task AllocateDisplayNumberAsync(string cafeId, CancellationToken ct) + { + var max = await _db.Orders + .Where(o => o.CafeId == cafeId) + .MaxAsync(o => (int?)o.DisplayNumber, ct); + return (max ?? 0) + 1; + } private static LiveOrderDto MapLive(Order o) => new( + o.Id, + o.DisplayNumber > 0 ? o.DisplayNumber : ReceiptPrintFormatting.StableDisplayNumberFromId(o.Id), + o.Status, + o.Table?.Number, + o.OrderType, + o.Total, + o.CreatedAt, + o.Items.Select(i => new OrderItemDto( + i.Id, + i.MenuItemId, + i.MenuItem?.Name ?? "", + i.Quantity, + i.UnitPrice, + i.Notes, + i.IsVoided, + i.VoidedAt)).ToList()); +} diff --git a/src/Meezi.API/Services/Delivery/DeliveryStatusSyncService.cs b/src/Meezi.API/Services/Delivery/DeliveryStatusSyncService.cs new file mode 100644 index 0000000..91a2206 --- /dev/null +++ b/src/Meezi.API/Services/Delivery/DeliveryStatusSyncService.cs @@ -0,0 +1,151 @@ +using Meezi.API.Models.Orders; +using Meezi.Core.Entities; +using Meezi.Core.Enums; +using Meezi.Core.Interfaces; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; + +namespace Meezi.API.Services.Delivery; + +public interface IDeliveryStatusSyncService +{ + Task SyncInternalStatusAsync( + string cafeId, + string orderId, + OrderStatus newStatus, + CancellationToken ct = default); + + Task ApplyPlatformStatusAsync( + DeliveryPlatform platform, + string externalOrderId, + string platformStatus, + CancellationToken ct = default); +} + +public class DeliveryStatusSyncService : IDeliveryStatusSyncService +{ + private readonly AppDbContext _db; + private readonly IKdsNotifier _kds; + private readonly ISnappfoodClient _snappfood; + private readonly ITap30Client _tap30; + private readonly IInventoryService _inventory; + private readonly ILogger _logger; + + public DeliveryStatusSyncService( + AppDbContext db, + IKdsNotifier kds, + ISnappfoodClient snappfood, + ITap30Client tap30, + IInventoryService inventory, + ILogger logger) + { + _db = db; + _kds = kds; + _snappfood = snappfood; + _tap30 = tap30; + _inventory = inventory; + _logger = logger; + } + + public async Task SyncInternalStatusAsync( + string cafeId, + string orderId, + OrderStatus newStatus, + CancellationToken ct = default) + { + var order = await _db.Orders + .FirstOrDefaultAsync(o => o.Id == orderId && o.CafeId == cafeId, ct); + + if (order?.DeliveryPlatform is null || string.IsNullOrEmpty(order.ExternalOrderId)) + return false; + + var platformStatus = MapToPlatformStatus(newStatus); + await NotifyPlatformAsync(order.DeliveryPlatform.Value, order.ExternalOrderId, platformStatus, ct); + + if (newStatus == OrderStatus.Delivered && !string.IsNullOrEmpty(order.SnappfoodOrderId)) + await _snappfood.NotifyOrderDeliveredAsync(order.SnappfoodOrderId, ct); + + return true; + } + + public async Task ApplyPlatformStatusAsync( + DeliveryPlatform platform, + string externalOrderId, + string platformStatus, + CancellationToken ct = default) + { + var order = await _db.Orders + .Include(o => o.Items) + .ThenInclude(i => i.MenuItem) + .FirstOrDefaultAsync( + o => o.DeliveryPlatform == platform && o.ExternalOrderId == externalOrderId, + ct); + + if (order is null) + return false; + + var mapped = MapFromPlatformStatus(platformStatus); + if (order.Status == mapped) + return true; + + if (mapped == OrderStatus.Cancelled) + await RollbackInventoryPlaceholderAsync(order, ct); + + order.Status = mapped; + await _db.SaveChangesAsync(ct); + + await _kds.NotifyOrderStatusChangedAsync(order.CafeId, order.Id, mapped, ct); + if (!string.IsNullOrEmpty(order.TableId)) + await _kds.NotifyTableStatusChangedAsync(order.CafeId, ct); + + return true; + } + + private async Task NotifyPlatformAsync( + DeliveryPlatform platform, + string externalOrderId, + string status, + CancellationToken ct) + { + switch (platform) + { + case DeliveryPlatform.Snappfood: + await _snappfood.NotifyOrderStatusAsync(externalOrderId, status, ct); + break; + case DeliveryPlatform.Tap30: + await _tap30.NotifyOrderStatusAsync(externalOrderId, status, ct); + break; + } + } + + private Task RollbackInventoryPlaceholderAsync(Order order, CancellationToken ct) + { + _logger.LogInformation( + "Delivery order {OrderId} cancelled — inventory rollback pending BOM linkage", + order.Id); + return Task.CompletedTask; + } + + private static string MapToPlatformStatus(OrderStatus status) => status switch + { + OrderStatus.Pending => "pending", + OrderStatus.Confirmed => "confirmed", + OrderStatus.Preparing => "preparing", + OrderStatus.Ready => "ready", + OrderStatus.Delivered => "delivered", + OrderStatus.Cancelled => "cancelled", + _ => "confirmed" + }; + + private static OrderStatus MapFromPlatformStatus(string platformStatus) => + platformStatus.Trim().ToLowerInvariant() switch + { + "pending" => OrderStatus.Pending, + "confirmed" => OrderStatus.Confirmed, + "preparing" or "in_progress" => OrderStatus.Preparing, + "ready" => OrderStatus.Ready, + "delivered" or "completed" => OrderStatus.Delivered, + "cancelled" or "canceled" => OrderStatus.Cancelled, + _ => OrderStatus.Confirmed + }; +} diff --git a/src/Meezi.API/Services/Delivery/DeliveryWebhookIngressService.cs b/src/Meezi.API/Services/Delivery/DeliveryWebhookIngressService.cs new file mode 100644 index 0000000..2c264bd --- /dev/null +++ b/src/Meezi.API/Services/Delivery/DeliveryWebhookIngressService.cs @@ -0,0 +1,77 @@ +using Hangfire; +using Meezi.API.Jobs; +using Meezi.Core.Delivery; +using Meezi.Core.Entities; +using Meezi.Core.Enums; +using Meezi.Infrastructure.Data; + +namespace Meezi.API.Services.Delivery; + +public record WebhookIngressResult(bool Accepted, string? WebhookLogId, string? ErrorCode, string? Message); + +public interface IDeliveryWebhookIngressService +{ + Task ReceiveAsync( + DeliveryPlatform platform, + string rawBody, + string? signatureHeader, + CancellationToken ct = default); +} + +public class DeliveryWebhookIngressService : IDeliveryWebhookIngressService +{ + private readonly AppDbContext _db; + private readonly IWebhookSignatureService _signatures; + private readonly IOrderNormalizer _normalizer; + + public DeliveryWebhookIngressService( + AppDbContext db, + IWebhookSignatureService signatures, + IOrderNormalizer normalizer) + { + _db = db; + _signatures = signatures; + _normalizer = normalizer; + } + + public async Task ReceiveAsync( + DeliveryPlatform platform, + string rawBody, + string? signatureHeader, + CancellationToken ct = default) + { + var signatureValid = _signatures.Verify(platform, rawBody, signatureHeader); + + var log = new WebhookLog + { + Id = $"wh_{Guid.NewGuid():N}"[..24], + Platform = platform, + RawBody = rawBody, + SignatureHeader = signatureHeader, + SignatureValid = signatureValid, + CreatedAt = DateTime.UtcNow + }; + + _db.WebhookLogs.Add(log); + await _db.SaveChangesAsync(ct); + + if (!signatureValid) + return new WebhookIngressResult(false, log.Id, "UNAUTHORIZED", "Invalid signature."); + + var unified = _normalizer.FromJson(platform, rawBody); + if (unified is null) + { + log.Success = false; + log.Processed = true; + log.ErrorMessage = "Could not normalize payload."; + log.ProcessedAt = DateTime.UtcNow; + await _db.SaveChangesAsync(ct); + return new WebhookIngressResult(false, log.Id, "VALIDATION_ERROR", log.ErrorMessage); + } + + BackgroundJob.Enqueue(job => + job.ExecuteAsync(log.Id, unified, CancellationToken.None)); + + return new WebhookIngressResult(true, log.Id, null, null); + } +} diff --git a/src/Meezi.API/Services/Delivery/OrderNormalizer.cs b/src/Meezi.API/Services/Delivery/OrderNormalizer.cs new file mode 100644 index 0000000..22f0aec --- /dev/null +++ b/src/Meezi.API/Services/Delivery/OrderNormalizer.cs @@ -0,0 +1,127 @@ +using System.Text.Json; +using Meezi.API.Models.Snappfood; +using Meezi.API.Models.Tap30; +using Meezi.Core.Delivery; +using Meezi.Core.Enums; + +namespace Meezi.API.Services.Delivery; + +public interface IOrderNormalizer +{ + UnifiedDeliveryOrder? FromSnappfood(SnappfoodWebhookOrder payload); + UnifiedDeliveryOrder? FromTap30(Tap30WebhookOrder payload); + UnifiedDeliveryOrder? FromJson(DeliveryPlatform platform, string rawJson); +} + +public class OrderNormalizer : IOrderNormalizer +{ + private static readonly JsonSerializerOptions JsonOptions = new() + { + PropertyNameCaseInsensitive = true + }; + + public UnifiedDeliveryOrder? FromSnappfood(SnappfoodWebhookOrder payload) + { + if (string.IsNullOrWhiteSpace(payload.OrderId) || string.IsNullOrWhiteSpace(payload.VendorId)) + return null; + + var items = payload.Items.Select(i => new UnifiedDeliveryItem( + Sku: i.Name, + Name: i.Name, + Quantity: i.Quantity, + UnitPrice: i.UnitPrice, + Notes: "Snappfood")).ToList(); + + if (items.Count == 0) + return null; + + return new UnifiedDeliveryOrder( + payload.OrderId.Trim(), + DeliveryPlatform.Snappfood, + payload.VendorId.Trim(), + DateTime.UtcNow, + new UnifiedDeliveryCustomer( + payload.CustomerName ?? "Snappfood", + payload.CustomerPhone ?? ""), + items, + new UnifiedDeliveryPayment( + payload.Total, + "online", + true, + null), + new UnifiedDeliveryInfo("delivery"), + UnifiedDeliveryStatus.Confirmed); + } + + public UnifiedDeliveryOrder? FromTap30(Tap30WebhookOrder payload) + { + if (string.IsNullOrWhiteSpace(payload.OrderId) || string.IsNullOrWhiteSpace(payload.VendorId)) + return null; + + var items = (payload.Items ?? []) + .Where(i => !string.IsNullOrWhiteSpace(i.Name) && i.Quantity > 0) + .Select(i => new UnifiedDeliveryItem( + Sku: i.Sku ?? i.Name, + Name: i.Name, + Quantity: i.Quantity, + UnitPrice: i.UnitPrice, + i.Notes)) + .ToList(); + + if (items.Count == 0) + return null; + + var customer = payload.Customer ?? new Tap30Customer(null, null, null, null, null); + var deliveryType = string.IsNullOrWhiteSpace(payload.DeliveryType) + ? "delivery" + : payload.DeliveryType.Trim().ToLowerInvariant(); + + return new UnifiedDeliveryOrder( + payload.OrderId.Trim(), + DeliveryPlatform.Tap30, + payload.VendorId.Trim(), + DateTime.UtcNow, + new UnifiedDeliveryCustomer( + customer.Name ?? "Tap30", + customer.Phone ?? "", + customer.Address, + customer.Lat, + customer.Lng), + items, + new UnifiedDeliveryPayment( + payload.Total, + payload.PaymentMethod ?? "online", + payload.IsPaid ?? true, + payload.Commission), + new UnifiedDeliveryInfo( + deliveryType, + payload.EstimatedMinutes, + payload.DriverName, + payload.DriverPhone), + MapTap30Status(payload.Status)); + } + + public UnifiedDeliveryOrder? FromJson(DeliveryPlatform platform, string rawJson) + { + return platform switch + { + DeliveryPlatform.Snappfood => FromSnappfood( + JsonSerializer.Deserialize(rawJson, JsonOptions)!), + DeliveryPlatform.Tap30 => FromTap30( + JsonSerializer.Deserialize(rawJson, JsonOptions)!), + _ => null + }; + } + + private static UnifiedDeliveryStatus MapTap30Status(string? status) => + status?.Trim().ToLowerInvariant() switch + { + "pending" => UnifiedDeliveryStatus.Pending, + "confirmed" => UnifiedDeliveryStatus.Confirmed, + "preparing" or "in_progress" => UnifiedDeliveryStatus.Preparing, + "ready" => UnifiedDeliveryStatus.Ready, + "delivered" or "completed" => UnifiedDeliveryStatus.Delivered, + "cancelled" or "canceled" => UnifiedDeliveryStatus.Cancelled, + _ => UnifiedDeliveryStatus.Confirmed + }; +} diff --git a/src/Meezi.API/Services/Delivery/WebhookSignatureService.cs b/src/Meezi.API/Services/Delivery/WebhookSignatureService.cs new file mode 100644 index 0000000..a3af39c --- /dev/null +++ b/src/Meezi.API/Services/Delivery/WebhookSignatureService.cs @@ -0,0 +1,55 @@ +using System.Security.Cryptography; +using System.Text; +using Meezi.API.Configuration; +using Meezi.Core.Enums; +using Microsoft.Extensions.Options; + +namespace Meezi.API.Services.Delivery; + +public interface IWebhookSignatureService +{ + bool Verify(DeliveryPlatform platform, string rawBody, string? signatureHeader); +} + +public class WebhookSignatureService : IWebhookSignatureService +{ + private readonly DeliveryPlatformsOptions _options; + + public WebhookSignatureService(IOptions options) + { + _options = options.Value; + } + + public bool Verify(DeliveryPlatform platform, string rawBody, string? signatureHeader) + { + var secret = platform switch + { + DeliveryPlatform.Snappfood => _options.Snappfood.WebhookSecret, + DeliveryPlatform.Tap30 => _options.Tap30.WebhookSecret, + DeliveryPlatform.Digikala => _options.Digikala.WebhookSecret, + _ => "" + }; + + if (string.IsNullOrWhiteSpace(secret)) + return true; + + if (string.IsNullOrWhiteSpace(signatureHeader)) + return false; + + var provided = signatureHeader.Trim(); + if (provided.StartsWith("sha256=", StringComparison.OrdinalIgnoreCase)) + provided = provided["sha256=".Length..]; + + var expected = ComputeHmacSha256Hex(rawBody, secret); + return CryptographicOperations.FixedTimeEquals( + Encoding.UTF8.GetBytes(expected), + Encoding.UTF8.GetBytes(provided)); + } + + public static string ComputeHmacSha256Hex(string body, string secret) + { + using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret)); + var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(body)); + return Convert.ToHexString(hash).ToLowerInvariant(); + } +} diff --git a/src/Meezi.API/Services/DiscoverNlpParser.cs b/src/Meezi.API/Services/DiscoverNlpParser.cs new file mode 100644 index 0000000..de37325 --- /dev/null +++ b/src/Meezi.API/Services/DiscoverNlpParser.cs @@ -0,0 +1,228 @@ +using Meezi.Core.Utilities; + +namespace Meezi.API.Services; + +/// +/// Parses a free-text Persian (or mixed) query into structured discover filter hints. +/// The results are merged into before matching, +/// so the AI text box silently pre-populates filters without the user having to pick chips. +/// +public static class DiscoverNlpParser +{ + public record NlpHints( + IReadOnlyList Themes, + IReadOnlyList Vibes, + IReadOnlyList Occasions, + IReadOnlyList SpaceFeatures, + string? NoiseLevel, + string? PriceTier, + string? Size); + + public static NlpHints Parse(string rawQuery) + { + if (string.IsNullOrWhiteSpace(rawQuery)) + return new NlpHints([], [], [], [], null, null, null); + + // Normalize: lower-case + Arabic glyph unification + var q = PersianSearchNormalizer.Normalize(rawQuery.ToLowerInvariant()); + + var themes = new HashSet(); + var vibes = new HashSet(); + var occasions = new HashSet(); + var spaceFeatures = new HashSet(); + string? noise = null; + string? price = null; + string? size = null; + + // ── Themes ────────────────────────────────────────────────────────── + if (Any(q, "کتاب", "کتابخانه", "book cafe", "book")) + themes.Add("book_cafe"); + + if (Any(q, "قهوه تخصصی", "رست", "اسپشلتی", "specialty", "singl origin", "سینگل اوریجین")) + themes.Add("specialty_coffee"); + + if (Any(q, "چایی", "چای", "tea house", "tea")) + themes.Add("tea_house"); + + if (Any(q, "گالری", "هنر", "نقاشی", "art gallery", "art")) + themes.Add("art_gallery"); + + if (Any(q, "ورزشی", "sport", "بازی", "game")) + themes.Add("sport_cafe"); + + if (Any(q, "گیمینگ", "gaming", "گیم")) + themes.Add("gaming_cafe"); + + if (Any(q, "سنتی", "قدیمی", "ایرانی", "اصیل", "persian", "ایران")) + themes.Add("persian_traditional"); + + if (Any(q, "رترو", "vintage", "کلاسیک")) + themes.Add("vintage"); + + if (Any(q, "مدرن", "شیک", "مینیمال", "modern", "minimal")) + themes.Add("modern"); + + if (Any(q, "صبحانه", "برانچ", "brunch")) + themes.Add("brunch"); + + if (Any(q, "شب", "دیروقت", "late night", "شبانه")) + themes.Add("late_night"); + + if (Any(q, "گیاه", "طبیعت", "سبز", "plants", "گلخانه")) + themes.Add("plants_heavy"); + + if (Any(q, "اینستا", "دکور", "عکس", "فتوگنیک", "insta")) + themes.Add("instagrammable"); + + if (Any(q, "کافه رستوران", "رستوران", "رستو")) + themes.Add("brunch"); + + // ── Vibes ──────────────────────────────────────────────────────────── + if (Any(q, "ارام", "ساکت", "بی سر و صدا", "آروم", "quiet")) + { + vibes.Add("quiet"); + noise ??= "quiet"; + } + + if (Any(q, "رمانتیک", "عاشقانه", "دوتایی", "romantic")) + vibes.Add("romantic"); + + if (Any(q, "دنج", "صمیمی", "گرم", "cozy", "کوزی")) + vibes.Add("cozy"); + + if (Any(q, "هنری", "خلاق", "artistic")) + vibes.Add("artistic"); + + if (Any(q, "شلوغ", "شاد", "پر انرژی", "lively")) + { + vibes.Add("lively"); + noise ??= "lively"; + } + + if (Any(q, "درس", "مطالعه", "کار", "لپتاپ", "study")) + vibes.Add("study_friendly"); + + if (Any(q, "لوکس", "لاکچری", "لوکژری", "luxury")) + vibes.Add("luxury"); + + if (Any(q, "ترند", "مد", "تازه", "trendy")) + vibes.Add("trendy"); + + // ── Occasions ──────────────────────────────────────────────────────── + if (Any(q, "قرار", "دیت", "دوتایی", "date")) + occasions.Add("date"); + + if (Any(q, "خانواده", "بچه", "کودک", "family")) + occasions.Add("family"); + + if (Any(q, "دوستانه", "دوستا", "جمعی", "friends", "گروهی")) + occasions.Add("friends"); + + if (Any(q, "مطالعه", "درس خوندن", "کار کردن", "study", "لپتاپ")) + occasions.Add("study_work"); + + if (Any(q, "جلسه", "بیزنس", "کاری", "business meeting", "میتینگ")) + occasions.Add("business_meeting"); + + if (Any(q, "صبحانه", "breakfast")) + occasions.Add("breakfast"); + + if (Any(q, "برانچ", "brunch")) + occasions.Add("brunch"); + + if (Any(q, "تولد", "جشن", "celebration", "مهمونی")) + occasions.Add("celebration"); + + if (Any(q, "تنها", "solo", "تک نفره", "یه نفره")) + occasions.Add("solo"); + + if (Any(q, "بعد شام", "after dinner", "شام")) + occasions.Add("after_dinner"); + + if (Any(q, "گروه بزرگ", "خیلی ها", "group", "گروه زیاد")) + occasions.Add("group_large"); + + if (Any(q, "آشنایی", "دوست پیدا", "finding someone")) + occasions.Add("finding_someone"); + + if (Any(q, "سریع", "quick", "یه قهوه سریع")) + occasions.Add("quick_coffee"); + + // ── Space Features ─────────────────────────────────────────────────── + if (Any(q, "وایفای", "وای فای", "wifi", "اینترنت")) + spaceFeatures.Add("wifi"); + + if (Any(q, "پارکینگ", "parking")) + spaceFeatures.Add("parking"); + + if (Any(q, "فضای باز", "بیرون", "خارج از ساختمان", "outdoor")) + spaceFeatures.Add("outdoor"); + + if (Any(q, "تراس", "terrace")) + spaceFeatures.Add("terrace"); + + if (Any(q, "روفتاپ", "روف تاپ", "بام", "rooftop", "پشت بام")) + spaceFeatures.Add("rooftop"); + + if (Any(q, "باغ", "گاردن", "garden")) + spaceFeatures.Add("garden"); + + if (Any(q, "سگ", "گربه", "حیوان خانگی", "pet", "پت فرندلی")) + spaceFeatures.Add("pet_friendly"); + + if (Any(q, "کودک", "بچه دار", "kids friendly", "بازی بچه")) + spaceFeatures.Add("kids_friendly"); + + if (Any(q, "موسیقی زنده", "live music", "کنسرت کوچک")) + spaceFeatures.Add("live_music"); + + if (Any(q, "اتاق خصوصی", "خصوصی", "private room")) + spaceFeatures.Add("private_room"); + + if (Any(q, "قلیان", "hookah", "نارگیله")) + spaceFeatures.Add("hookah"); + + if (Any(q, "بازی رومیزی", "بازی فکری", "board game")) + spaceFeatures.Add("board_games"); + + if (Any(q, "بدون دود", "سیگار ممنوع", "non smoking", "no smoking", "دود نه")) + spaceFeatures.Add("no_smoking"); + + if (Any(q, "نمازخانه", "نماز", "prayer")) + spaceFeatures.Add("prayer_room"); + + if (Any(q, "بیرون بر", "تیک اوی", "takeaway", "تیک اوت")) + spaceFeatures.Add("takeaway"); + + if (Any(q, "ویلچر", "معلول", "دسترسی", "wheelchair")) + spaceFeatures.Add("wheelchair"); + + // ── Price ──────────────────────────────────────────────────────────── + if (Any(q, "ارزون", "ارزان", "مقرون به صرفه", "budget", "اقتصادی", "کم هزینه")) + price = "budget"; + else if (Any(q, "لوکس", "گرون", "لاکچری", "premium", "پریمیوم", "گران قیمت")) + price = "premium"; + + // ── Size ───────────────────────────────────────────────────────────── + if (Any(q, "کوچیک", "کوچک", "tiny", "کوچولو", "مینی")) + size = "tiny"; + else if (Any(q, "دنج", "cozy", "صمیمی") && size is null) + size = "cozy"; + else if (Any(q, "بزرگ", "وسیع", "large", "spacious", "گنجایش بالا")) + size = "large"; + + return new NlpHints( + [.. themes], + [.. vibes], + [.. occasions], + [.. spaceFeatures], + noise, + price, + size); + } + + // ── helpers ────────────────────────────────────────────────────────────── + + private static bool Any(string q, params string[] terms) => + terms.Any(q.Contains); +} diff --git a/src/Meezi.API/Services/DiscoverProfileMatcher.cs b/src/Meezi.API/Services/DiscoverProfileMatcher.cs new file mode 100644 index 0000000..e9a169b --- /dev/null +++ b/src/Meezi.API/Services/DiscoverProfileMatcher.cs @@ -0,0 +1,212 @@ +using Meezi.Core.Discover; + +namespace Meezi.API.Services; + +public record DiscoverFilterParams( + string? City = null, + string? Q = null, + double? MinRating = null, + string? Sort = null, + IReadOnlyList? Themes = null, + IReadOnlyList? Vibes = null, + IReadOnlyList? Occasions = null, + IReadOnlyList? SpaceFeatures = null, + string? NoiseLevel = null, + string? PriceTier = null, + string? Size = null, + bool RequireProfile = false, + bool OpenNow = false) +{ + public static DiscoverFilterParams FromQuery( + string? city, + string? q, + double? minRating, + string? sort, + string? themes, + string? vibes, + string? occasions, + string? spaceFeatures, + string? noise, + string? priceTier, + string? size, + bool requireProfile, + bool openNow) + { + // Parse explicit CSV filter chips + var explicitThemes = SplitCsv(themes); + var explicitVibes = SplitCsv(vibes); + var explicitOcc = SplitCsv(occasions); + var explicitSpace = SplitCsv(spaceFeatures); + var explicitNoise = NormalizeToken(noise); + var explicitPrice = NormalizeToken(priceTier); + var explicitSize = NormalizeToken(size); + + // AI-parse the free-text query and merge hints (explicit chips take priority) + if (!string.IsNullOrWhiteSpace(q)) + { + var hints = DiscoverNlpParser.Parse(q); + explicitThemes = Merge(explicitThemes, hints.Themes); + explicitVibes = Merge(explicitVibes, hints.Vibes); + explicitOcc = Merge(explicitOcc, hints.Occasions); + explicitSpace = Merge(explicitSpace, hints.SpaceFeatures); + explicitNoise ??= hints.NoiseLevel; + explicitPrice ??= hints.PriceTier; + explicitSize ??= hints.Size; + } + + return new( + city, + q, + minRating, + sort, + explicitThemes, + explicitVibes, + explicitOcc, + explicitSpace, + explicitNoise, + explicitPrice, + explicitSize, + requireProfile, + openNow); + } + + private static IReadOnlyList? SplitCsv(string? value) + { + if (string.IsNullOrWhiteSpace(value)) return null; + var parts = value + .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Select(NormalizeToken) + .Where(p => !string.IsNullOrEmpty(p)) + .Select(p => p!) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + return parts.Count == 0 ? null : parts; + } + + private static IReadOnlyList? Merge(IReadOnlyList? existing, IReadOnlyList hints) + { + if (hints.Count == 0) return existing; + if (existing is null or { Count: 0 }) return hints.Count > 0 ? hints : null; + var merged = existing.Union(hints, StringComparer.OrdinalIgnoreCase).ToList(); + return merged.Count > 0 ? merged : null; + } + + private static string? NormalizeToken(string? value) => + string.IsNullOrWhiteSpace(value) ? null : value.Trim().ToLowerInvariant(); +} + +public static class DiscoverProfileMatcher +{ + public static bool HasMeaningfulProfile(CafeDiscoverProfile profile) => + profile.Themes.Count > 0 + || profile.Vibes.Count > 0 + || profile.Occasions.Count > 0 + || profile.SpaceFeatures.Count > 0 + || !string.IsNullOrWhiteSpace(profile.Size) + || !string.IsNullOrWhiteSpace(profile.NoiseLevel) + || !string.IsNullOrWhiteSpace(profile.PriceTier); + + /// + /// Relevance score in [0, 1]. Returns 1.0 when no profile filters are set (text-only search). + /// Each filter dimension that matches contributes 1 / totalDimensions to the score. + /// Score ≥ passes the soft-filter. + /// + public const double MinScoreThreshold = 0.25; + + public static double Score(CafeDiscoverProfile profile, DiscoverFilterParams filters) + { + int total = 0; + int matched = 0; + + if (filters.Themes is { Count: > 0 } themes) + { + total++; + if (themes.Any(t => profile.Themes.Contains(t, StringComparer.OrdinalIgnoreCase))) + matched++; + } + + if (filters.Vibes is { Count: > 0 } vibes) + { + total++; + if (vibes.Any(v => profile.Vibes.Contains(v, StringComparer.OrdinalIgnoreCase))) + matched++; + } + + if (filters.Occasions is { Count: > 0 } occasions) + { + total++; + if (occasions.Any(o => profile.Occasions.Contains(o, StringComparer.OrdinalIgnoreCase))) + matched++; + } + + if (filters.SpaceFeatures is { Count: > 0 } space) + { + total++; + if (space.Any(s => profile.SpaceFeatures.Contains(s, StringComparer.OrdinalIgnoreCase))) + matched++; + } + + if (!string.IsNullOrEmpty(filters.NoiseLevel)) + { + total++; + if (string.Equals(profile.NoiseLevel, filters.NoiseLevel, StringComparison.OrdinalIgnoreCase)) + matched++; + } + + if (!string.IsNullOrEmpty(filters.PriceTier)) + { + total++; + if (string.Equals(profile.PriceTier, filters.PriceTier, StringComparison.OrdinalIgnoreCase)) + matched++; + } + + if (!string.IsNullOrEmpty(filters.Size)) + { + total++; + if (string.Equals(profile.Size, filters.Size, StringComparison.OrdinalIgnoreCase)) + matched++; + } + + return total == 0 ? 1.0 : (double)matched / total; + } + + /// + /// Legacy hard-AND match still used for explicit chip-only searches (no free text). + /// Free-text searches use ≥ threshold instead. + /// + public static bool Matches(CafeDiscoverProfile profile, DiscoverFilterParams filters) + { + if (filters.RequireProfile && !HasMeaningfulProfile(profile)) + return false; + + if (filters.Themes is { Count: > 0 } themes + && !themes.Any(t => profile.Themes.Contains(t, StringComparer.OrdinalIgnoreCase))) + return false; + + if (filters.Vibes is { Count: > 0 } vibes + && !vibes.Any(v => profile.Vibes.Contains(v, StringComparer.OrdinalIgnoreCase))) + return false; + + if (filters.Occasions is { Count: > 0 } occasions + && !occasions.Any(o => profile.Occasions.Contains(o, StringComparer.OrdinalIgnoreCase))) + return false; + + if (filters.SpaceFeatures is { Count: > 0 } space + && !space.Any(s => profile.SpaceFeatures.Contains(s, StringComparer.OrdinalIgnoreCase))) + return false; + + if (!string.IsNullOrEmpty(filters.NoiseLevel) + && !string.Equals(profile.NoiseLevel, filters.NoiseLevel, StringComparison.OrdinalIgnoreCase)) + return false; + + if (!string.IsNullOrEmpty(filters.PriceTier) + && !string.Equals(profile.PriceTier, filters.PriceTier, StringComparison.OrdinalIgnoreCase)) + return false; + + if (!string.IsNullOrEmpty(filters.Size) + && !string.Equals(profile.Size, filters.Size, StringComparison.OrdinalIgnoreCase)) + return false; + + return true; + } +} diff --git a/src/Meezi.API/Services/ExpenseService.cs b/src/Meezi.API/Services/ExpenseService.cs new file mode 100644 index 0000000..3e595d3 --- /dev/null +++ b/src/Meezi.API/Services/ExpenseService.cs @@ -0,0 +1,185 @@ +using Meezi.API.Models.Expenses; +using Meezi.Core.Entities; +using Meezi.Core.Enums; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; + +namespace Meezi.API.Services; + +public record ExpenseServiceResult(bool Success, T? Data, string? ErrorCode = null, string? Field = null); + +public interface IExpenseService +{ + Task> CreateExpenseAsync( + string cafeId, + CreateExpenseRequest request, + string userId, + CancellationToken cancellationToken = default); + + Task GetExpensesAsync( + string cafeId, + string branchId, + DateOnly from, + DateOnly to, + int page, + int pageSize, + CancellationToken cancellationToken = default); + + Task> DeleteExpenseAsync( + string cafeId, + string expenseId, + CancellationToken cancellationToken = default); +} + +public class ExpenseService : IExpenseService +{ + private readonly AppDbContext _db; + private readonly IShiftService _shifts; + + public ExpenseService(AppDbContext db, IShiftService shifts) + { + _db = db; + _shifts = shifts; + } + + public async Task> CreateExpenseAsync( + string cafeId, + CreateExpenseRequest request, + string userId, + CancellationToken cancellationToken = default) + { + var branch = await _db.Branches.FirstOrDefaultAsync( + b => b.Id == request.BranchId && b.CafeId == cafeId && b.IsActive, + cancellationToken); + if (branch is null) + return new ExpenseServiceResult(false, null, "BRANCH_NOT_FOUND", "branchId"); + + if (!string.IsNullOrWhiteSpace(request.ShiftId)) + { + var shift = await _db.RegisterShifts.FirstOrDefaultAsync( + s => s.Id == request.ShiftId && s.CafeId == cafeId, + cancellationToken); + if (shift is null) + return new ExpenseServiceResult(false, null, "SHIFT_NOT_FOUND", "shiftId"); + if (shift.BranchId != request.BranchId) + return new ExpenseServiceResult(false, null, "SHIFT_BRANCH_MISMATCH", "shiftId"); + if (shift.Status != ShiftStatus.Open) + return new ExpenseServiceResult(false, null, "SHIFT_ALREADY_CLOSED", "shiftId"); + } + + var now = DateTime.UtcNow; + var expense = new Expense + { + CafeId = cafeId, + BranchId = request.BranchId, + ShiftId = request.ShiftId, + Category = request.Category, + Amount = request.Amount, + Note = request.Note, + ReceiptImageUrl = request.ReceiptImageUrl, + CreatedByUserId = userId, + CreatedAt = now + }; + + _db.Expenses.Add(expense); + await _db.SaveChangesAsync(cancellationToken); + + if (!string.IsNullOrWhiteSpace(request.ShiftId)) + { + var withdrawalNote = $"expense:{expense.Category}"; + if (!string.IsNullOrWhiteSpace(request.Note)) + withdrawalNote += $" — {request.Note}"; + + var tx = await _shifts.RecordTransactionAsync( + cafeId, + request.ShiftId, + CashTransactionType.Withdrawal, + PaymentMethod.Cash, + request.Amount, + userId, + referenceId: expense.Id, + note: withdrawalNote, + cancellationToken); + + if (!tx.Success) + { + _db.Expenses.Remove(expense); + await _db.SaveChangesAsync(cancellationToken); + return new ExpenseServiceResult( + false, null, tx.ErrorCode ?? "WITHDRAWAL_FAILED", tx.Field); + } + } + + return new ExpenseServiceResult(true, ToDto(expense)); + } + + public async Task GetExpensesAsync( + string cafeId, + string branchId, + DateOnly from, + DateOnly to, + int page, + int pageSize, + CancellationToken cancellationToken = default) + { + page = Math.Max(1, page); + pageSize = Math.Clamp(pageSize, 1, 100); + + var (utcStart, _) = IranCalendar.GetUtcRangeForIranDay(from); + var (_, utcEnd) = IranCalendar.GetUtcRangeForIranDay(to); + + var query = _db.Expenses.AsNoTracking() + .Where(e => e.CafeId == cafeId + && e.BranchId == branchId + && e.CreatedAt >= utcStart + && e.CreatedAt < utcEnd); + + var total = await query.CountAsync(cancellationToken); + var rows = await query + .OrderByDescending(e => e.CreatedAt) + .Skip((page - 1) * pageSize) + .Take(pageSize) + .ToListAsync(cancellationToken); + + return new ExpenseListResult(rows.Select(ToDto).ToList(), total); + } + + public async Task> DeleteExpenseAsync( + string cafeId, + string expenseId, + CancellationToken cancellationToken = default) + { + var expense = await _db.Expenses.FirstOrDefaultAsync( + e => e.Id == expenseId && e.CafeId == cafeId, + cancellationToken); + + if (expense is null) + return new ExpenseServiceResult(false, false, "NOT_FOUND"); + + var now = DateTime.UtcNow; + expense.DeletedAt = now; + + var withdrawal = await _db.CashTransactions.FirstOrDefaultAsync( + t => t.CafeId == cafeId + && t.ReferenceId == expenseId + && t.Type == CashTransactionType.Withdrawal, + cancellationToken); + if (withdrawal is not null) + withdrawal.DeletedAt = now; + + await _db.SaveChangesAsync(cancellationToken); + return new ExpenseServiceResult(true, true); + } + + private static ExpenseDto ToDto(Expense e) => new( + e.Id, + e.CafeId, + e.BranchId, + e.ShiftId, + e.Category, + e.Amount, + e.Note, + e.ReceiptImageUrl, + e.CreatedByUserId, + e.CreatedAt); +} diff --git a/src/Meezi.API/Services/HrService.cs b/src/Meezi.API/Services/HrService.cs new file mode 100644 index 0000000..6e1210d --- /dev/null +++ b/src/Meezi.API/Services/HrService.cs @@ -0,0 +1,336 @@ +using Meezi.API.Models.Hr; +using Meezi.Core.Entities; +using Meezi.Core.Enums; +using Meezi.Core.Interfaces; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; + +namespace Meezi.API.Services; + +public interface IHrService +{ + Task> GetEmployeesAsync( + string cafeId, + string? branchId = null, + CancellationToken cancellationToken = default); + Task GetEmployeeAsync(string cafeId, string employeeId, CancellationToken cancellationToken = default); + Task EmployeeBelongsToCafeAsync(string cafeId, string employeeId, CancellationToken cancellationToken = default); + + Task GetTodayShiftAsync(string cafeId, string employeeId, CancellationToken cancellationToken = default); + Task ClockInAsync(string cafeId, string employeeId, CancellationToken cancellationToken = default); + Task ClockOutAsync(string cafeId, string employeeId, CancellationToken cancellationToken = default); + Task> GetAttendanceAsync(string cafeId, string? employeeId, DateOnly? from, DateOnly? to, CancellationToken cancellationToken = default); + + Task> GetShiftsAsync(string cafeId, string employeeId, CancellationToken cancellationToken = default); + Task> UpsertShiftsAsync(string cafeId, string employeeId, UpsertShiftsRequest request, CancellationToken cancellationToken = default); + + Task> GetLeaveRequestsAsync(string cafeId, LeaveStatus? status, CancellationToken cancellationToken = default); + Task CreateLeaveRequestAsync(string cafeId, string employeeId, CreateLeaveRequest request, CancellationToken cancellationToken = default); + Task ReviewLeaveRequestAsync(string cafeId, string leaveId, string reviewerId, ReviewLeaveRequest request, CancellationToken cancellationToken = default); + + Task> GetSalariesAsync(string cafeId, string? monthYear, CancellationToken cancellationToken = default); + Task CreateSalaryAsync(string cafeId, CreateSalaryRequest request, CancellationToken cancellationToken = default); + Task MarkSalaryPaidAsync(string cafeId, string salaryId, CancellationToken cancellationToken = default); +} + +public class HrService : IHrService +{ + private readonly AppDbContext _db; + + public HrService(AppDbContext db) + { + _db = db; + } + + public async Task> GetEmployeesAsync( + string cafeId, + string? branchId = null, + CancellationToken cancellationToken = default) + { + var query = _db.Employees.Where(e => e.CafeId == cafeId); + if (!string.IsNullOrEmpty(branchId)) + query = query.Where(e => e.BranchId == branchId); + var list = await query.OrderBy(e => e.Name).ToListAsync(cancellationToken); + return list.Select(e => new EmployeeSummaryDto(e.Id, e.Name, e.Phone, e.Role, e.BaseSalary)).ToList(); + } + + public async Task GetEmployeeAsync(string cafeId, string employeeId, CancellationToken cancellationToken = default) + { + var e = await _db.Employees.FirstOrDefaultAsync(x => x.Id == employeeId && x.CafeId == cafeId, cancellationToken); + return e is null ? null : new EmployeeSummaryDto(e.Id, e.Name, e.Phone, e.Role, e.BaseSalary); + } + + public Task EmployeeBelongsToCafeAsync(string cafeId, string employeeId, CancellationToken cancellationToken = default) => + _db.Employees.AnyAsync(e => e.Id == employeeId && e.CafeId == cafeId, cancellationToken); + + public async Task GetTodayShiftAsync(string cafeId, string employeeId, CancellationToken cancellationToken = default) + { + if (!await EmployeeBelongsToCafeAsync(cafeId, employeeId, cancellationToken)) + return null; + + var day = (int)DateTime.UtcNow.DayOfWeek; + var shift = await _db.EmployeeSchedules + .FirstOrDefaultAsync(s => s.EmployeeId == employeeId && s.DayOfWeek == day, cancellationToken); + + var type = shift?.ShiftType ?? ShiftType.DayOff; + return new TodayShiftDto(type, ShiftLabel(type)); + } + + public async Task ClockInAsync(string cafeId, string employeeId, CancellationToken cancellationToken = default) + { + var employee = await _db.Employees + .FirstOrDefaultAsync(e => e.Id == employeeId && e.CafeId == cafeId, cancellationToken); + if (employee is null) return null; + + var today = DateOnly.FromDateTime(DateTime.UtcNow); + var attendance = await _db.Attendances + .FirstOrDefaultAsync(a => a.EmployeeId == employeeId && a.Date == today, cancellationToken); + + if (attendance is null) + { + attendance = new Attendance + { + EmployeeId = employeeId, + Date = today, + ClockIn = DateTime.UtcNow + }; + _db.Attendances.Add(attendance); + } + else if (attendance.ClockIn is null) + { + attendance.ClockIn = DateTime.UtcNow; + } + else + { + return ToAttendanceDto(attendance, employee.Name); + } + + await _db.SaveChangesAsync(cancellationToken); + return ToAttendanceDto(attendance, employee.Name); + } + + public async Task ClockOutAsync(string cafeId, string employeeId, CancellationToken cancellationToken = default) + { + var employee = await _db.Employees + .FirstOrDefaultAsync(e => e.Id == employeeId && e.CafeId == cafeId, cancellationToken); + if (employee is null) return null; + + var today = DateOnly.FromDateTime(DateTime.UtcNow); + var attendance = await _db.Attendances + .FirstOrDefaultAsync(a => a.EmployeeId == employeeId && a.Date == today, cancellationToken); + + if (attendance?.ClockIn is null) return null; + + attendance.ClockOut = DateTime.UtcNow; + await _db.SaveChangesAsync(cancellationToken); + return ToAttendanceDto(attendance, employee.Name); + } + + public async Task> GetAttendanceAsync( + string cafeId, + string? employeeId, + DateOnly? from, + DateOnly? to, + CancellationToken cancellationToken = default) + { + var query = _db.Attendances + .Include(a => a.Employee) + .Where(a => a.Employee.CafeId == cafeId); + + if (!string.IsNullOrEmpty(employeeId)) + query = query.Where(a => a.EmployeeId == employeeId); + + if (from.HasValue) + query = query.Where(a => a.Date >= from.Value); + if (to.HasValue) + query = query.Where(a => a.Date <= to.Value); + + var list = await query.OrderByDescending(a => a.Date).Take(100).ToListAsync(cancellationToken); + return list.Select(a => ToAttendanceDto(a, a.Employee.Name)).ToList(); + } + + public async Task> GetShiftsAsync(string cafeId, string employeeId, CancellationToken cancellationToken = default) + { + if (!await EmployeeBelongsToCafeAsync(cafeId, employeeId, cancellationToken)) + return []; + + var shifts = await _db.EmployeeSchedules + .Where(s => s.EmployeeId == employeeId) + .OrderBy(s => s.DayOfWeek) + .ToListAsync(cancellationToken); + return shifts.Select(s => new ShiftDto(s.DayOfWeek, s.ShiftType)).ToList(); + } + + public async Task> UpsertShiftsAsync( + string cafeId, + string employeeId, + UpsertShiftsRequest request, + CancellationToken cancellationToken = default) + { + if (!await EmployeeBelongsToCafeAsync(cafeId, employeeId, cancellationToken)) + return []; + + var existing = await _db.EmployeeSchedules.Where(s => s.EmployeeId == employeeId).ToListAsync(cancellationToken); + _db.EmployeeSchedules.RemoveRange(existing); + + foreach (var s in request.Shifts) + { + _db.EmployeeSchedules.Add(new EmployeeSchedule + { + EmployeeId = employeeId, + DayOfWeek = s.DayOfWeek, + ShiftType = s.ShiftType + }); + } + + await _db.SaveChangesAsync(cancellationToken); + return request.Shifts.ToList(); + } + + public async Task> GetLeaveRequestsAsync( + string cafeId, + LeaveStatus? status, + CancellationToken cancellationToken = default) + { + var query = _db.LeaveRequests + .Include(l => l.Employee) + .Where(l => l.Employee.CafeId == cafeId); + + if (status.HasValue) + query = query.Where(l => l.Status == status.Value); + + var list = await query.OrderByDescending(l => l.CreatedAt).ToListAsync(cancellationToken); + return list.Select(l => ToLeaveDto(l)).ToList(); + } + + public async Task CreateLeaveRequestAsync( + string cafeId, + string employeeId, + CreateLeaveRequest request, + CancellationToken cancellationToken = default) + { + if (!await EmployeeBelongsToCafeAsync(cafeId, employeeId, cancellationToken)) + return null; + + var employee = await _db.Employees.FindAsync([employeeId], cancellationToken); + var entity = new LeaveRequest + { + EmployeeId = employeeId, + StartDate = request.StartDate, + EndDate = request.EndDate, + Reason = request.Reason, + Status = LeaveStatus.Pending + }; + + _db.LeaveRequests.Add(entity); + await _db.SaveChangesAsync(cancellationToken); + return ToLeaveDto(entity, employee!.Name); + } + + public async Task ReviewLeaveRequestAsync( + string cafeId, + string leaveId, + string reviewerId, + ReviewLeaveRequest request, + CancellationToken cancellationToken = default) + { + var entity = await _db.LeaveRequests + .Include(l => l.Employee) + .FirstOrDefaultAsync(l => l.Id == leaveId && l.Employee.CafeId == cafeId, cancellationToken); + + if (entity is null) return null; + + entity.Status = request.Status; + entity.ReviewedBy = reviewerId; + await _db.SaveChangesAsync(cancellationToken); + return ToLeaveDto(entity); + } + + public async Task> GetSalariesAsync( + string cafeId, + string? monthYear, + CancellationToken cancellationToken = default) + { + var query = _db.EmployeeSalaries + .Include(s => s.Employee) + .Where(s => s.Employee.CafeId == cafeId); + + if (!string.IsNullOrEmpty(monthYear)) + query = query.Where(s => s.MonthYear == monthYear); + + var list = await query.OrderByDescending(s => s.MonthYear).ToListAsync(cancellationToken); + return list.Select(s => ToSalaryDto(s)).ToList(); + } + + public async Task CreateSalaryAsync( + string cafeId, + CreateSalaryRequest request, + CancellationToken cancellationToken = default) + { + if (!await EmployeeBelongsToCafeAsync(cafeId, request.EmployeeId, cancellationToken)) + return null; + + var employee = await _db.Employees.FindAsync([request.EmployeeId], cancellationToken); + var net = request.BaseSalary + request.OvertimePay - request.Deductions; + + var entity = new EmployeeSalary + { + EmployeeId = request.EmployeeId, + MonthYear = request.MonthYear, + BaseSalary = request.BaseSalary, + OvertimePay = request.OvertimePay, + Deductions = request.Deductions, + NetSalary = net, + IsPaid = false + }; + + _db.EmployeeSalaries.Add(entity); + await _db.SaveChangesAsync(cancellationToken); + return ToSalaryDto(entity, employee!.Name); + } + + public async Task MarkSalaryPaidAsync(string cafeId, string salaryId, CancellationToken cancellationToken = default) + { + var entity = await _db.EmployeeSalaries + .Include(s => s.Employee) + .FirstOrDefaultAsync(s => s.Id == salaryId && s.Employee.CafeId == cafeId, cancellationToken); + + if (entity is null) return null; + + entity.IsPaid = true; + await _db.SaveChangesAsync(cancellationToken); + return ToSalaryDto(entity); + } + + private static string ShiftLabel(ShiftType type) => type switch + { + ShiftType.Morning => "08:00 - 16:00", + ShiftType.Evening => "16:00 - 00:00", + _ => "Day off" + }; + + private static AttendanceDto ToAttendanceDto(Attendance a, string name) => new( + a.Id, a.EmployeeId, name, a.Date, a.ClockIn, a.ClockOut, a.Notes); + + private static LeaveRequestDto ToLeaveDto(LeaveRequest l, string? name = null) => new( + l.Id, + l.EmployeeId, + name ?? l.Employee?.Name ?? "", + l.StartDate, + l.EndDate, + l.Reason, + l.Status, + l.ReviewedBy, + l.CreatedAt); + + private static EmployeeSalaryDto ToSalaryDto(EmployeeSalary s, string? name = null) => new( + s.Id, + s.EmployeeId, + name ?? s.Employee?.Name ?? "", + s.MonthYear, + s.BaseSalary, + s.OvertimePay, + s.Deductions, + s.NetSalary, + s.IsPaid); +} diff --git a/src/Meezi.API/Services/IAuthService.cs b/src/Meezi.API/Services/IAuthService.cs new file mode 100644 index 0000000..7cf48f3 --- /dev/null +++ b/src/Meezi.API/Services/IAuthService.cs @@ -0,0 +1,18 @@ +using Meezi.API.Models.Auth; + +namespace Meezi.API.Services; + +public interface IAuthService +{ + Task<(bool Success, SendOtpResponse? Data, string? ErrorCode, string? ErrorMessage)> SendOtpAsync( + SendOtpRequest request, + CancellationToken cancellationToken = default); + + Task<(bool Success, AuthTokenResponse? Data, string? ErrorCode, string? ErrorMessage)> VerifyOtpAsync( + VerifyOtpRequest request, + CancellationToken cancellationToken = default); + + Task<(bool Success, AuthTokenResponse? Data, string? ErrorCode, string? ErrorMessage)> RefreshAsync( + RefreshTokenRequest request, + CancellationToken cancellationToken = default); +} diff --git a/src/Meezi.API/Services/IJwtTokenService.cs b/src/Meezi.API/Services/IJwtTokenService.cs new file mode 100644 index 0000000..852120c --- /dev/null +++ b/src/Meezi.API/Services/IJwtTokenService.cs @@ -0,0 +1,14 @@ +using Meezi.Core.Entities; +using Meezi.Core.Enums; + +namespace Meezi.API.Services; + +public interface IJwtTokenService +{ + string CreateAccessToken(Employee employee, Cafe cafe); + string CreateConsumerAccessToken(ConsumerAccount account, string language = "fa"); + string CreateRefreshToken(); + DateTime GetAccessTokenExpiry(); +} + +public record TokenPair(string AccessToken, string RefreshToken, DateTime AccessTokenExpiresAt); diff --git a/src/Meezi.API/Services/IKdsNotifier.cs b/src/Meezi.API/Services/IKdsNotifier.cs new file mode 100644 index 0000000..b6517bc --- /dev/null +++ b/src/Meezi.API/Services/IKdsNotifier.cs @@ -0,0 +1,10 @@ +using Meezi.API.Models.Orders; + +namespace Meezi.API.Services; + +public interface IKdsNotifier +{ + Task NotifyOrderCreatedAsync(string cafeId, LiveOrderDto order, CancellationToken cancellationToken = default); + Task NotifyOrderStatusChangedAsync(string cafeId, string orderId, Core.Enums.OrderStatus status, CancellationToken cancellationToken = default); + Task NotifyTableStatusChangedAsync(string cafeId, CancellationToken cancellationToken = default); +} diff --git a/src/Meezi.API/Services/IOrderNotificationService.cs b/src/Meezi.API/Services/IOrderNotificationService.cs new file mode 100644 index 0000000..133066b --- /dev/null +++ b/src/Meezi.API/Services/IOrderNotificationService.cs @@ -0,0 +1,12 @@ +using Meezi.API.Models.Orders; +using Meezi.Core.Entities; +using Meezi.Core.Enums; + +namespace Meezi.API.Services; + +public interface IOrderNotificationService +{ + Task NotifyGuestOrderPlacedAsync(Order order, LiveOrderDto live, CancellationToken ct = default); + Task NotifyOrderStatusChangedAsync(Order order, CancellationToken ct = default); + Task NotifyCallWaiterAsync(string cafeId, string tableId, string tableNumber, CancellationToken ct = default); +} diff --git a/src/Meezi.API/Services/InventoryService.cs b/src/Meezi.API/Services/InventoryService.cs new file mode 100644 index 0000000..1c2f684 --- /dev/null +++ b/src/Meezi.API/Services/InventoryService.cs @@ -0,0 +1,550 @@ +using Microsoft.AspNetCore.SignalR; +using Meezi.API.Hubs; +using Meezi.API.Models.Notifications; +using Meezi.Core.Entities; +using Meezi.Core.Enums; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; + +namespace Meezi.API.Services; + +public record IngredientDto( + string Id, + string Name, + string Unit, + decimal QuantityOnHand, + decimal ReorderLevel, + decimal UnitCost, + decimal ParLevel, + decimal LowStockWarningPercent, + decimal WarningThreshold, + decimal StockValueToman, + bool IsLowStock); + +public record CreateIngredientRequest( + string Name, + string Unit, + decimal QuantityOnHand, + decimal ReorderLevel, + decimal UnitCost, + decimal ParLevel, + decimal LowStockWarningPercent, + decimal? TotalPaidToman = null, + string? BranchId = null); + +public record UpdateIngredientRequest( + string? Name, + string? Unit, + decimal? ReorderLevel, + decimal? UnitCost, + decimal? ParLevel, + decimal? LowStockWarningPercent); + +public record AdjustStockRequest( + decimal Delta, + string? Note, + decimal? TotalPaidToman = null, + string? BranchId = null); + +public record InventoryPurchaseDto( + string Id, + string IngredientId, + string IngredientName, + decimal Delta, + string Unit, + decimal TotalPaidToman, + decimal UnitCostAfter, + DateTime CreatedAt, + string? ExpenseId); + +public record InventoryPurchasesSummaryDto( + decimal TotalPaidToman, + int PurchaseCount, + IReadOnlyList Recent); + +public record RecipeLineDto( + string Id, + string IngredientId, + string IngredientName, + string Unit, + decimal QuantityPerUnit); + +public record MenuItemRecipeDto( + string MenuItemId, + string MenuItemName, + IReadOnlyList Lines, + decimal MaterialCostPerUnitToman); + +public record SetRecipeLineRequest(string IngredientId, decimal QuantityPerUnit); + +public record SetMenuItemRecipeRequest(IReadOnlyList Lines); + +public record OrderDeductionResult( + bool Applied, + IReadOnlyList LowStockIngredientNames); + +public interface IInventoryService +{ + Task> ListAsync(string cafeId, CancellationToken ct = default); + Task> LowStockAsync(string cafeId, CancellationToken ct = default); + Task CreateAsync(string cafeId, CreateIngredientRequest request, CancellationToken ct = default); + Task UpdateAsync(string cafeId, string ingredientId, UpdateIngredientRequest request, CancellationToken ct = default); + Task AdjustAsync( + string cafeId, + string ingredientId, + AdjustStockRequest request, + string? userId, + CancellationToken ct = default); + Task GetPurchasesSummaryAsync( + string cafeId, + string branchId, + DateOnly from, + DateOnly to, + CancellationToken ct = default); + Task GetRecipeAsync(string cafeId, string menuItemId, CancellationToken ct = default); + Task SetRecipeAsync(string cafeId, string menuItemId, SetMenuItemRecipeRequest request, CancellationToken ct = default); + Task DeductForOrderAsync( + string cafeId, + string orderId, + IReadOnlyList<(string MenuItemId, int Quantity)> lines, + CancellationToken ct = default); +} + +public class InventoryService : IInventoryService +{ + private readonly AppDbContext _db; + private readonly IHubContext _kdsHub; + + public InventoryService(AppDbContext db, IHubContext kdsHub) + { + _db = db; + _kdsHub = kdsHub; + } + + public async Task> ListAsync(string cafeId, CancellationToken ct = default) + { + var rows = await _db.Ingredients.AsNoTracking() + .Where(i => i.CafeId == cafeId) + .OrderBy(i => i.Name) + .ToListAsync(ct); + return rows.Select(ToDto).ToList(); + } + + public async Task> LowStockAsync(string cafeId, CancellationToken ct = default) + { + var rows = await _db.Ingredients.Where(i => i.CafeId == cafeId).ToListAsync(ct); + return rows.Where(IsLowStock).Select(ToDto).ToList(); + } + + public async Task CreateAsync(string cafeId, CreateIngredientRequest request, CancellationToken ct = default) + { + var par = request.ParLevel > 0 ? request.ParLevel : request.QuantityOnHand; + var unitCost = ResolveUnitCost(request.QuantityOnHand, request.UnitCost, request.TotalPaidToman); + var entity = new Ingredient + { + Id = $"ing_{Guid.NewGuid():N}"[..24], + CafeId = cafeId, + Name = request.Name.Trim(), + Unit = string.IsNullOrWhiteSpace(request.Unit) ? "عدد" : request.Unit.Trim(), + QuantityOnHand = request.QuantityOnHand, + ReorderLevel = request.ReorderLevel, + UnitCost = unitCost, + ParLevel = par, + LowStockWarningPercent = ClampPercent(request.LowStockWarningPercent) + }; + _db.Ingredients.Add(entity); + if (request.QuantityOnHand != 0) + { + var movement = NewMovement( + cafeId, + entity.Id, + request.QuantityOnHand, + request.TotalPaidToman > 0 ? StockMovementKind.Purchase : StockMovementKind.Manual, + null, + request.TotalPaidToman > 0 ? "خرید اولیه" : "موجودی اولیه", + request.TotalPaidToman, + request.BranchId); + _db.StockMovements.Add(movement); + + if (request.TotalPaidToman > 0 && !string.IsNullOrWhiteSpace(request.BranchId)) + { + var expense = await TryCreatePurchaseExpenseAsync( + cafeId, + request.BranchId, + request.TotalPaidToman.Value, + $"خرید انبار: {entity.Name} ({request.QuantityOnHand:N0} {entity.Unit})", + userId: null, + ct); + if (expense is not null) + movement.ExpenseId = expense.Id; + } + } + + await _db.SaveChangesAsync(ct); + return ToDto(entity); + } + + public async Task UpdateAsync( + string cafeId, + string ingredientId, + UpdateIngredientRequest request, + CancellationToken ct = default) + { + var entity = await _db.Ingredients.FirstOrDefaultAsync(i => i.Id == ingredientId && i.CafeId == cafeId, ct); + if (entity is null) return null; + + if (!string.IsNullOrWhiteSpace(request.Name)) entity.Name = request.Name.Trim(); + if (!string.IsNullOrWhiteSpace(request.Unit)) entity.Unit = request.Unit.Trim(); + if (request.ReorderLevel.HasValue) entity.ReorderLevel = request.ReorderLevel.Value; + if (request.UnitCost.HasValue) entity.UnitCost = request.UnitCost.Value; + if (request.ParLevel.HasValue) entity.ParLevel = request.ParLevel.Value; + if (request.LowStockWarningPercent.HasValue) + entity.LowStockWarningPercent = ClampPercent(request.LowStockWarningPercent.Value); + + await _db.SaveChangesAsync(ct); + return ToDto(entity); + } + + public async Task AdjustAsync( + string cafeId, + string ingredientId, + AdjustStockRequest request, + string? userId, + CancellationToken ct = default) + { + var entity = await _db.Ingredients.FirstOrDefaultAsync(i => i.Id == ingredientId && i.CafeId == cafeId, ct); + if (entity is null) return null; + + if (request.Delta > 0) + { + if (request.TotalPaidToman is null or <= 0) + throw new InvalidOperationException("TOTAL_PAID_REQUIRED"); + if (string.IsNullOrWhiteSpace(request.BranchId)) + throw new InvalidOperationException("BRANCH_ID_REQUIRED"); + + var oldQty = entity.QuantityOnHand; + var oldValue = oldQty * entity.UnitCost; + entity.QuantityOnHand += request.Delta; + entity.UnitCost = entity.QuantityOnHand > 0 + ? (oldValue + request.TotalPaidToman.Value) / entity.QuantityOnHand + : request.TotalPaidToman.Value / request.Delta; + + var movement = NewMovement( + cafeId, + ingredientId, + request.Delta, + StockMovementKind.Purchase, + null, + request.Note?.Trim() ?? "خرید / ورود به انبار", + request.TotalPaidToman, + request.BranchId); + _db.StockMovements.Add(movement); + + var expense = await TryCreatePurchaseExpenseAsync( + cafeId, + request.BranchId!, + request.TotalPaidToman.Value, + $"خرید انبار: {entity.Name} ({request.Delta:N0} {entity.Unit})", + userId, + ct); + if (expense is not null) + movement.ExpenseId = expense.Id; + } + else + { + entity.QuantityOnHand += request.Delta; + _db.StockMovements.Add(NewMovement( + cafeId, + ingredientId, + request.Delta, + StockMovementKind.Manual, + null, + request.Note?.Trim() ?? "تنظیم دستی", + null)); + } + + await _db.SaveChangesAsync(ct); + + if (IsLowStock(entity)) + await NotifyLowStockAsync(cafeId, [entity], ct); + + return ToDto(entity); + } + + public async Task GetPurchasesSummaryAsync( + string cafeId, + string branchId, + DateOnly from, + DateOnly to, + CancellationToken ct = default) + { + var utcStart = from.ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc); + var utcEnd = to.AddDays(1).ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc); + + var movements = await _db.StockMovements.AsNoTracking() + .Include(m => m.Ingredient) + .Where(m => m.CafeId == cafeId + && m.BranchId == branchId + && m.TotalCostToman != null + && m.TotalCostToman > 0 + && m.CreatedAt >= utcStart + && m.CreatedAt < utcEnd) + .OrderByDescending(m => m.CreatedAt) + .Take(50) + .ToListAsync(ct); + + var total = movements.Sum(m => m.TotalCostToman ?? 0); + var recent = movements.Select(m => new InventoryPurchaseDto( + m.Id, + m.IngredientId, + m.Ingredient.Name, + m.Delta, + m.Ingredient.Unit, + m.TotalCostToman ?? 0, + m.Ingredient.UnitCost, + m.CreatedAt, + m.ExpenseId)).ToList(); + + return new InventoryPurchasesSummaryDto(total, recent.Count, recent); + } + + public async Task GetRecipeAsync(string cafeId, string menuItemId, CancellationToken ct = default) + { + var item = await _db.MenuItems.AsNoTracking() + .FirstOrDefaultAsync(m => m.Id == menuItemId && m.CafeId == cafeId, ct); + if (item is null) return null; + + var lines = await _db.MenuItemIngredients.AsNoTracking() + .Include(r => r.Ingredient) + .Where(r => r.CafeId == cafeId && r.MenuItemId == menuItemId) + .ToListAsync(ct); + + return BuildRecipeDto(item, lines); + } + + public async Task SetRecipeAsync( + string cafeId, + string menuItemId, + SetMenuItemRecipeRequest request, + CancellationToken ct = default) + { + var item = await _db.MenuItems.FirstOrDefaultAsync(m => m.Id == menuItemId && m.CafeId == cafeId, ct); + if (item is null) return null; + + var existing = await _db.MenuItemIngredients + .Where(r => r.CafeId == cafeId && r.MenuItemId == menuItemId) + .ToListAsync(ct); + _db.MenuItemIngredients.RemoveRange(existing); + + foreach (var line in request.Lines.Where(l => l.QuantityPerUnit > 0)) + { + var ingOk = await _db.Ingredients.AnyAsync(i => i.Id == line.IngredientId && i.CafeId == cafeId, ct); + if (!ingOk) continue; + + _db.MenuItemIngredients.Add(new MenuItemIngredient + { + Id = $"mii_{Guid.NewGuid():N}"[..24], + CafeId = cafeId, + MenuItemId = menuItemId, + IngredientId = line.IngredientId, + QuantityPerUnit = line.QuantityPerUnit + }); + } + + await _db.SaveChangesAsync(ct); + return await GetRecipeAsync(cafeId, menuItemId, ct); + } + + public async Task DeductForOrderAsync( + string cafeId, + string orderId, + IReadOnlyList<(string MenuItemId, int Quantity)> lines, + CancellationToken ct = default) + { + if (lines.Count == 0) + return new OrderDeductionResult(false, []); + + var menuItemIds = lines.Select(l => l.MenuItemId).Distinct().ToList(); + var recipes = await _db.MenuItemIngredients + .Where(r => r.CafeId == cafeId && menuItemIds.Contains(r.MenuItemId)) + .ToListAsync(ct); + + if (recipes.Count == 0) + return new OrderDeductionResult(false, []); + + var usage = new Dictionary(StringComparer.Ordinal); + foreach (var line in lines) + { + foreach (var recipe in recipes.Where(r => r.MenuItemId == line.MenuItemId)) + { + var amount = recipe.QuantityPerUnit * line.Quantity; + usage[recipe.IngredientId] = usage.GetValueOrDefault(recipe.IngredientId) + amount; + } + } + + if (usage.Count == 0) + return new OrderDeductionResult(false, []); + + var ingredientIds = usage.Keys.ToList(); + var ingredients = await _db.Ingredients + .Where(i => i.CafeId == cafeId && ingredientIds.Contains(i.Id)) + .ToListAsync(ct); + + foreach (var ing in ingredients) + { + if (!usage.TryGetValue(ing.Id, out var deduct)) continue; + ing.QuantityOnHand -= deduct; + _db.StockMovements.Add(NewMovement( + cafeId, + ing.Id, + -deduct, + StockMovementKind.OrderDeduction, + orderId, + $"سفارش {orderId[..Math.Min(8, orderId.Length)]}", + null)); + } + + await _db.SaveChangesAsync(ct); + + var lowStock = ingredients.Where(IsLowStock).ToList(); + if (lowStock.Count > 0) + await NotifyLowStockAsync(cafeId, lowStock, ct); + + return new OrderDeductionResult( + true, + lowStock.Select(i => i.Name).ToList()); + } + + private async Task NotifyLowStockAsync(string cafeId, IReadOnlyList items, CancellationToken ct) + { + if (items.Count == 0) return; + + var names = string.Join("، ", items.Select(i => $"{i.Name} ({FormatQty(i)})")); + var notification = new CafeNotification + { + CafeId = cafeId, + Type = "inventory_low_stock", + Title = "کمبود مواد اولیه", + Body = names + }; + _db.CafeNotifications.Add(notification); + await _db.SaveChangesAsync(ct); + + var dto = new CafeNotificationDto( + notification.Id, + notification.Type, + notification.Title, + notification.Body, + notification.ReferenceId, + notification.TableNumber, + notification.IsRead, + notification.CreatedAt); + + await _kdsHub.Clients.Group(KdsHub.GroupName(cafeId)) + .SendAsync("NotificationReceived", dto, ct); + } + + private async Task TryCreatePurchaseExpenseAsync( + string cafeId, + string branchId, + decimal amount, + string note, + string? userId, + CancellationToken ct) + { + var branch = await _db.Branches.FirstOrDefaultAsync( + b => b.Id == branchId && b.CafeId == cafeId && b.IsActive, + ct); + if (branch is null) return null; + + var expense = new Expense + { + Id = $"exp_{Guid.NewGuid():N}"[..24], + CafeId = cafeId, + BranchId = branchId, + Category = ExpenseCategory.Supplies, + Amount = amount, + Note = note, + CreatedByUserId = userId ?? "system", + CreatedAt = DateTime.UtcNow + }; + _db.Expenses.Add(expense); + return expense; + } + + private static decimal ResolveUnitCost(decimal qty, decimal unitCost, decimal? totalPaid) + { + if (totalPaid is > 0 && qty > 0) + return totalPaid.Value / qty; + return unitCost; + } + + private static StockMovement NewMovement( + string cafeId, + string ingredientId, + decimal delta, + StockMovementKind kind, + string? orderId, + string? note, + decimal? totalCostToman, + string? branchId = null) => new() + { + Id = $"stk_{Guid.NewGuid():N}"[..24], + CafeId = cafeId, + IngredientId = ingredientId, + BranchId = branchId, + Delta = delta, + Kind = kind, + OrderId = orderId, + Note = note, + TotalCostToman = totalCostToman > 0 ? totalCostToman : null + }; + + private static MenuItemRecipeDto BuildRecipeDto(MenuItem item, List lines) + { + var recipeLines = lines.Select(r => new RecipeLineDto( + r.Id, + r.IngredientId, + r.Ingredient.Name, + r.Ingredient.Unit, + r.QuantityPerUnit)).ToList(); + + var cost = lines.Sum(r => r.QuantityPerUnit * r.Ingredient.UnitCost); + return new MenuItemRecipeDto(item.Id, item.Name, recipeLines, cost); + } + + private static bool IsLowStock(Ingredient i) + { + var threshold = WarningThreshold(i); + return i.QuantityOnHand <= threshold; + } + + private static decimal WarningThreshold(Ingredient i) + { + if (i.ParLevel > 0 && i.LowStockWarningPercent > 0) + return i.ParLevel * (i.LowStockWarningPercent / 100m); + return i.ReorderLevel; + } + + private static IngredientDto ToDto(Ingredient i) + { + var threshold = WarningThreshold(i); + return new IngredientDto( + i.Id, + i.Name, + i.Unit, + i.QuantityOnHand, + i.ReorderLevel, + i.UnitCost, + i.ParLevel, + i.LowStockWarningPercent, + threshold, + i.QuantityOnHand * i.UnitCost, + i.QuantityOnHand <= threshold); + } + + private static decimal ClampPercent(decimal p) => Math.Clamp(p, 1m, 100m); + + private static string FormatQty(Ingredient i) => + $"{i.QuantityOnHand:N0} {i.Unit}"; +} diff --git a/src/Meezi.API/Services/IranCalendar.cs b/src/Meezi.API/Services/IranCalendar.cs new file mode 100644 index 0000000..319a217 --- /dev/null +++ b/src/Meezi.API/Services/IranCalendar.cs @@ -0,0 +1,26 @@ +namespace Meezi.API.Services; + +public static class IranCalendar +{ + private static readonly TimeZoneInfo IranTz = TimeZoneInfo.FindSystemTimeZoneById( + OperatingSystem.IsWindows() ? "Iran Standard Time" : "Asia/Tehran"); + + public static DateOnly TodayInIran => + DateOnly.FromDateTime(TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, IranTz)); + + public static DateTime NowInIran => + TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, IranTz); + + public static TimeZoneInfo TimeZone => IranTz; + + public static (DateTime UtcStart, DateTime UtcEndExclusive) GetUtcRangeForIranDay(DateOnly date) + { + var localStart = date.ToDateTime(TimeOnly.MinValue); + var localEnd = date.AddDays(1).ToDateTime(TimeOnly.MinValue); + var utcStart = TimeZoneInfo.ConvertTimeToUtc( + DateTime.SpecifyKind(localStart, DateTimeKind.Unspecified), IranTz); + var utcEnd = TimeZoneInfo.ConvertTimeToUtc( + DateTime.SpecifyKind(localEnd, DateTimeKind.Unspecified), IranTz); + return (utcStart, utcEnd); + } +} diff --git a/src/Meezi.API/Services/JwtTokenService.cs b/src/Meezi.API/Services/JwtTokenService.cs new file mode 100644 index 0000000..5348724 --- /dev/null +++ b/src/Meezi.API/Services/JwtTokenService.cs @@ -0,0 +1,92 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using Meezi.Core.Constants; +using Meezi.Core.Entities; +using ConsumerAccount = Meezi.Core.Entities.ConsumerAccount; +using Microsoft.IdentityModel.Tokens; + +namespace Meezi.API.Services; + +public class JwtTokenService : IJwtTokenService +{ + private readonly IConfiguration _configuration; + + public JwtTokenService(IConfiguration configuration) + { + _configuration = configuration; + } + + public string CreateAccessToken(Employee employee, Cafe cafe) + { + var key = _configuration["Jwt:Key"] ?? throw new InvalidOperationException("Jwt:Key is not configured."); + var issuer = _configuration["Jwt:Issuer"] ?? "meezi"; + var audience = _configuration["Jwt:Audience"] ?? "meezi"; + var expiryDays = _configuration.GetValue("Jwt:AccessTokenExpiryDays", 7); + + var claims = new List + { + new(JwtRegisteredClaimNames.Sub, employee.Id), + new(MeeziClaimTypes.CafeId, cafe.Id), + new(MeeziClaimTypes.Role, employee.Role.ToString()), + new(MeeziClaimTypes.PlanTier, cafe.PlanTier.ToString()), + new(MeeziClaimTypes.Language, cafe.PreferredLanguage), + new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString("N")) + }; + + if (!string.IsNullOrEmpty(employee.BranchId)) + claims.Add(new Claim(MeeziClaimTypes.BranchId, employee.BranchId)); + + var credentials = new SigningCredentials( + new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key)), + SecurityAlgorithms.HmacSha256); + + var token = new JwtSecurityToken( + issuer, + audience, + claims, + expires: DateTime.UtcNow.AddDays(expiryDays), + signingCredentials: credentials); + + return new JwtSecurityTokenHandler().WriteToken(token); + } + + public string CreateConsumerAccessToken(ConsumerAccount account, string language = "fa") + { + var key = _configuration["Jwt:Key"] ?? throw new InvalidOperationException("Jwt:Key is not configured."); + var issuer = _configuration["Jwt:Issuer"] ?? "meezi"; + var audience = _configuration["Jwt:Audience"] ?? "meezi"; + var expiryDays = _configuration.GetValue("Jwt:AccessTokenExpiryDays", 7); + + var claims = new List + { + new(JwtRegisteredClaimNames.Sub, account.Id), + new(MeeziClaimTypes.Role, MeeziRoles.Customer), + new(MeeziClaimTypes.Actor, MeeziActorKinds.Consumer), + new(MeeziClaimTypes.Phone, account.Phone), + new(MeeziClaimTypes.Language, language), + new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString("N")) + }; + + var credentials = new SigningCredentials( + new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key)), + SecurityAlgorithms.HmacSha256); + + var token = new JwtSecurityToken( + issuer, + audience, + claims, + expires: DateTime.UtcNow.AddDays(expiryDays), + signingCredentials: credentials); + + return new JwtSecurityTokenHandler().WriteToken(token); + } + + public string CreateRefreshToken() => Guid.NewGuid().ToString("N") + Guid.NewGuid().ToString("N"); + + public DateTime GetAccessTokenExpiry() + { + var expiryDays = _configuration.GetValue("Jwt:AccessTokenExpiryDays", 7); + return DateTime.UtcNow.AddDays(expiryDays); + } +} diff --git a/src/Meezi.API/Services/KdsNotifier.cs b/src/Meezi.API/Services/KdsNotifier.cs new file mode 100644 index 0000000..a1807e8 --- /dev/null +++ b/src/Meezi.API/Services/KdsNotifier.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.SignalR; +using Meezi.API.Hubs; +using Meezi.API.Models.Orders; +using Meezi.Core.Enums; + +namespace Meezi.API.Services; + +public class KdsNotifier : IKdsNotifier +{ + private readonly IHubContext _hubContext; + + public KdsNotifier(IHubContext hubContext) + { + _hubContext = hubContext; + } + + public Task NotifyOrderCreatedAsync(string cafeId, LiveOrderDto order, CancellationToken cancellationToken = default) => + _hubContext.Clients.Group(KdsHub.GroupName(cafeId)).SendAsync("OrderCreated", order, cancellationToken); + + public Task NotifyOrderStatusChangedAsync(string cafeId, string orderId, OrderStatus status, CancellationToken cancellationToken = default) => + _hubContext.Clients.Group(KdsHub.GroupName(cafeId)).SendAsync("OrderStatusChanged", new { orderId, status }, cancellationToken); + + public Task NotifyTableStatusChangedAsync(string cafeId, CancellationToken cancellationToken = default) => + _hubContext.Clients.Group(KdsHub.GroupName(cafeId)).SendAsync("TableStatusChanged", null, cancellationToken); +} diff --git a/src/Meezi.API/Services/KitchenStationService.cs b/src/Meezi.API/Services/KitchenStationService.cs new file mode 100644 index 0000000..4e248f8 --- /dev/null +++ b/src/Meezi.API/Services/KitchenStationService.cs @@ -0,0 +1,142 @@ +using Meezi.API.Models.Kitchen; +using Meezi.Core.Entities; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; + +namespace Meezi.API.Services; + +public interface IKitchenStationService +{ + Task> ListAsync(string cafeId, CancellationToken ct = default); + Task CreateAsync(string cafeId, CreateKitchenStationRequest request, CancellationToken ct = default); + Task UpdateAsync(string cafeId, string id, UpdateKitchenStationRequest request, CancellationToken ct = default); + Task DeleteAsync(string cafeId, string id, CancellationToken ct = default); +} + +public class KitchenStationService : IKitchenStationService +{ + private readonly AppDbContext _db; + + public KitchenStationService(AppDbContext db) => _db = db; + + public async Task> ListAsync(string cafeId, CancellationToken ct = default) + { + var stations = await _db.KitchenStations + .Where(s => s.CafeId == cafeId) + .OrderBy(s => s.SortOrder) + .ThenBy(s => s.Name) + .Select(s => new + { + s.Id, + s.BranchId, + s.Name, + s.PrinterIp, + s.PrinterPort, + s.SortOrder, + CategoryCount = s.Categories.Count(c => c.DeletedAt == null) + }) + .ToListAsync(ct); + + return stations.Select(s => new KitchenStationDto( + s.Id, s.BranchId, s.Name, s.PrinterIp, s.PrinterPort, s.SortOrder, s.CategoryCount)).ToList(); + } + + public async Task CreateAsync( + string cafeId, + CreateKitchenStationRequest request, + CancellationToken ct = default) + { + if (!string.IsNullOrEmpty(request.BranchId)) + { + var branchOk = await _db.Branches.AnyAsync( + b => b.Id == request.BranchId && b.CafeId == cafeId, ct); + if (!branchOk) return null; + } + + var entity = new KitchenStation + { + CafeId = cafeId, + BranchId = request.BranchId, + Name = request.Name.Trim(), + PrinterIp = string.IsNullOrWhiteSpace(request.PrinterIp) ? null : request.PrinterIp.Trim(), + PrinterPort = request.PrinterPort > 0 ? request.PrinterPort : 9100, + SortOrder = request.SortOrder + }; + + _db.KitchenStations.Add(entity); + await _db.SaveChangesAsync(ct); + return await MapAsync(cafeId, entity.Id, ct); + } + + public async Task UpdateAsync( + string cafeId, + string id, + UpdateKitchenStationRequest request, + CancellationToken ct = default) + { + var entity = await _db.KitchenStations + .FirstOrDefaultAsync(s => s.Id == id && s.CafeId == cafeId, ct); + if (entity is null) return null; + + if (request.Name is not null) entity.Name = request.Name.Trim(); + if (request.BranchId is not null) + { + if (!string.IsNullOrEmpty(request.BranchId)) + { + var branchOk = await _db.Branches.AnyAsync( + b => b.Id == request.BranchId && b.CafeId == cafeId, ct); + if (!branchOk) return null; + } + + entity.BranchId = string.IsNullOrEmpty(request.BranchId) ? null : request.BranchId; + } + + if (request.PrinterIp is not null) + entity.PrinterIp = string.IsNullOrWhiteSpace(request.PrinterIp) ? null : request.PrinterIp.Trim(); + if (request.PrinterPort.HasValue) + entity.PrinterPort = request.PrinterPort.Value > 0 ? request.PrinterPort.Value : 9100; + if (request.SortOrder.HasValue) + entity.SortOrder = request.SortOrder.Value; + + await _db.SaveChangesAsync(ct); + return await MapAsync(cafeId, id, ct); + } + + public async Task DeleteAsync(string cafeId, string id, CancellationToken ct = default) + { + var entity = await _db.KitchenStations + .FirstOrDefaultAsync(s => s.Id == id && s.CafeId == cafeId, ct); + if (entity is null) return false; + + var categories = await _db.MenuCategories + .Where(c => c.KitchenStationId == id && c.CafeId == cafeId) + .ToListAsync(ct); + foreach (var cat in categories) + cat.KitchenStationId = null; + + entity.DeletedAt = DateTime.UtcNow; + await _db.SaveChangesAsync(ct); + return true; + } + + private async Task MapAsync(string cafeId, string id, CancellationToken ct) + { + var s = await _db.KitchenStations + .Where(x => x.Id == id && x.CafeId == cafeId) + .Select(x => new + { + x.Id, + x.BranchId, + x.Name, + x.PrinterIp, + x.PrinterPort, + x.SortOrder, + CategoryCount = x.Categories.Count(c => c.DeletedAt == null) + }) + .FirstOrDefaultAsync(ct); + + return s is null + ? null + : new KitchenStationDto(s.Id, s.BranchId, s.Name, s.PrinterIp, s.PrinterPort, s.SortOrder, s.CategoryCount); + } +} diff --git a/src/Meezi.API/Services/LoyaltyService.cs b/src/Meezi.API/Services/LoyaltyService.cs new file mode 100644 index 0000000..7fb769e --- /dev/null +++ b/src/Meezi.API/Services/LoyaltyService.cs @@ -0,0 +1,88 @@ +using Meezi.Core.Entities; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; + +namespace Meezi.API.Services; + +public sealed record LoyaltyRedeemResult(int PointsUsed, decimal DiscountToman); + +public interface ILoyaltyService +{ + /// Earn points when an order is fully paid (1 point per 10,000 ت paid). + Task ApplyEarnOnOrderPaidAsync(string cafeId, string? customerId, decimal paidAmount, CancellationToken ct = default); + + /// Redeem loyalty points before payment (1 point = 100 ت discount). + Task<(bool Success, LoyaltyRedeemResult? Data, string? ErrorCode)> RedeemOnOrderAsync( + string cafeId, + Order order, + int pointsRequested, + CancellationToken ct = default); +} + +public class LoyaltyService : ILoyaltyService +{ + public const decimal TomanPerPointEarn = 10_000m; + public const decimal TomanPerPointRedeem = 100m; + + private readonly AppDbContext _db; + + public LoyaltyService(AppDbContext db) => _db = db; + + public async Task ApplyEarnOnOrderPaidAsync( + string cafeId, + string? customerId, + decimal paidAmount, + CancellationToken ct = default) + { + if (string.IsNullOrEmpty(customerId) || paidAmount <= 0) + return; + + var points = (int)Math.Floor(paidAmount / TomanPerPointEarn); + if (points <= 0) + return; + + var customer = await _db.Customers + .FirstOrDefaultAsync(c => c.Id == customerId && c.CafeId == cafeId, ct); + if (customer is null) + return; + + customer.LoyaltyPoints += points; + await _db.SaveChangesAsync(ct); + } + + public async Task<(bool Success, LoyaltyRedeemResult? Data, string? ErrorCode)> RedeemOnOrderAsync( + string cafeId, + Order order, + int pointsRequested, + CancellationToken ct = default) + { + if (pointsRequested <= 0) + return (true, new LoyaltyRedeemResult(0, 0), null); + + if (string.IsNullOrEmpty(order.CustomerId)) + return (false, null, "LOYALTY_NO_CUSTOMER"); + + var customer = await _db.Customers + .FirstOrDefaultAsync(c => c.Id == order.CustomerId && c.CafeId == cafeId, ct); + if (customer is null) + return (false, null, "LOYALTY_NO_CUSTOMER"); + + var paidSoFar = order.Payments + .Where(p => p.Status == Core.Enums.PaymentStatus.Completed) + .Sum(p => p.Amount); + var amountDue = Math.Max(0, order.Total - paidSoFar); + if (amountDue <= 0) + return (false, null, "LOYALTY_NOTHING_DUE"); + + var maxByDue = (int)Math.Floor(amountDue / TomanPerPointRedeem); + var points = Math.Min(pointsRequested, Math.Min(customer.LoyaltyPoints, maxByDue)); + if (points <= 0) + return (false, null, "LOYALTY_INSUFFICIENT_POINTS"); + + var discount = points * TomanPerPointRedeem; + customer.LoyaltyPoints -= points; + await _db.SaveChangesAsync(ct); + + return (true, new LoyaltyRedeemResult(points, discount), null); + } +} diff --git a/src/Meezi.API/Services/MediaStorageService.cs b/src/Meezi.API/Services/MediaStorageService.cs new file mode 100644 index 0000000..9b475cf --- /dev/null +++ b/src/Meezi.API/Services/MediaStorageService.cs @@ -0,0 +1,147 @@ +namespace Meezi.API.Services; + +public interface IMediaStorageService +{ + Task SaveMenuImageAsync(string cafeId, IFormFile file, CancellationToken cancellationToken = default); + Task SaveMenuVideoAsync(string cafeId, IFormFile file, CancellationToken cancellationToken = default); + Task SaveTableImageAsync(string cafeId, IFormFile file, CancellationToken cancellationToken = default); + Task SaveTableVideoAsync(string cafeId, IFormFile file, CancellationToken cancellationToken = default); + Task SaveCafeLogoAsync(string cafeId, IFormFile file, CancellationToken cancellationToken = default); + Task SaveCafeCoverAsync(string cafeId, IFormFile file, CancellationToken cancellationToken = default); + Task SaveMenuModel3dAsync(string cafeId, IFormFile file, CancellationToken cancellationToken = default); + Task SaveMenuModel3dFromBytesAsync(string cafeId, byte[] glbBytes, CancellationToken cancellationToken = default); + Task SaveReviewPhotoAsync(string cafeId, IFormFile file, CancellationToken cancellationToken = default); + Task SaveCafeGalleryPhotoAsync(string cafeId, IFormFile file, CancellationToken cancellationToken = default); +} + +public class MediaStorageService : IMediaStorageService +{ + private static readonly HashSet ImageMime = new(StringComparer.OrdinalIgnoreCase) + { + "image/jpeg", "image/png", "image/webp" + }; + + private static readonly HashSet VideoMime = new(StringComparer.OrdinalIgnoreCase) + { + "video/mp4", "video/webm", "video/quicktime" + }; + + private const long MaxImageBytes = 5 * 1024 * 1024; + private const long MaxVideoBytes = 25 * 1024 * 1024; + private const long MaxModel3dBytes = 8 * 1024 * 1024; + + private static readonly HashSet Model3dMime = new(StringComparer.OrdinalIgnoreCase) + { + "model/gltf-binary", "application/octet-stream" + }; + + private readonly IWebHostEnvironment _env; + private readonly ILogger _logger; + + public MediaStorageService(IWebHostEnvironment env, ILogger logger) + { + _env = env; + _logger = logger; + } + + public Task SaveMenuImageAsync(string cafeId, IFormFile file, CancellationToken cancellationToken = default) + => SaveAsync(cafeId, file, "menu_img", ImageMime, MaxImageBytes, cancellationToken); + + public Task SaveMenuVideoAsync(string cafeId, IFormFile file, CancellationToken cancellationToken = default) + => SaveAsync(cafeId, file, "menu_vid", VideoMime, MaxVideoBytes, cancellationToken); + + public Task SaveTableImageAsync(string cafeId, IFormFile file, CancellationToken cancellationToken = default) + => SaveAsync(cafeId, file, "table_img", ImageMime, MaxImageBytes, cancellationToken); + + public Task SaveTableVideoAsync(string cafeId, IFormFile file, CancellationToken cancellationToken = default) + => SaveAsync(cafeId, file, "table_vid", VideoMime, MaxVideoBytes, cancellationToken); + + public Task SaveCafeLogoAsync(string cafeId, IFormFile file, CancellationToken cancellationToken = default) + => SaveAsync(cafeId, file, "logo", ImageMime, MaxImageBytes, cancellationToken); + + public Task SaveCafeCoverAsync(string cafeId, IFormFile file, CancellationToken cancellationToken = default) + => SaveAsync(cafeId, file, "cover", ImageMime, MaxImageBytes, cancellationToken); + + public Task SaveReviewPhotoAsync(string cafeId, IFormFile file, CancellationToken cancellationToken = default) + => SaveAsync(cafeId, file, "review", ImageMime, MaxImageBytes, cancellationToken); + + public Task SaveCafeGalleryPhotoAsync(string cafeId, IFormFile file, CancellationToken cancellationToken = default) + => SaveAsync(cafeId, file, "gallery", ImageMime, MaxImageBytes, cancellationToken); + + public Task SaveMenuModel3dAsync(string cafeId, IFormFile file, CancellationToken cancellationToken = default) + => SaveModel3dAsync(cafeId, file, cancellationToken); + + public async Task SaveMenuModel3dFromBytesAsync( + string cafeId, + byte[] glbBytes, + CancellationToken cancellationToken = default) + { + if (glbBytes.Length == 0 || glbBytes.Length > MaxModel3dBytes) return null; + + var dir = Path.Combine(_env.ContentRootPath, "uploads", cafeId); + Directory.CreateDirectory(dir); + var savedName = $"menu_3d_ai_{Guid.NewGuid():N}.glb"; + var path = Path.Combine(dir, savedName); + + await File.WriteAllBytesAsync(path, glbBytes, cancellationToken); + _logger.LogInformation("Saved AI 3D model for cafe {CafeId}", cafeId); + return $"/uploads/{cafeId}/{savedName}"; + } + + private async Task SaveModel3dAsync( + string cafeId, + IFormFile file, + CancellationToken cancellationToken) + { + if (file.Length == 0 || file.Length > MaxModel3dBytes) return null; + + var fileName = file.FileName.ToLowerInvariant(); + var isGlb = fileName.EndsWith(".glb", StringComparison.OrdinalIgnoreCase) + || Model3dMime.Contains(file.ContentType); + if (!isGlb) return null; + + var dir = Path.Combine(_env.ContentRootPath, "uploads", cafeId); + Directory.CreateDirectory(dir); + var savedName = $"menu_3d_{Guid.NewGuid():N}.glb"; + var path = Path.Combine(dir, savedName); + + await using var stream = File.Create(path); + await file.CopyToAsync(stream, cancellationToken); + + _logger.LogInformation("Saved 3D model media for cafe {CafeId}", cafeId); + return $"/uploads/{cafeId}/{savedName}"; + } + + private async Task SaveAsync( + string cafeId, + IFormFile file, + string prefix, + HashSet allowedMime, + long maxBytes, + CancellationToken cancellationToken) + { + if (file.Length == 0 || file.Length > maxBytes) return null; + if (!allowedMime.Contains(file.ContentType)) return null; + + var ext = file.ContentType.ToLowerInvariant() switch + { + "image/png" => ".png", + "image/webp" => ".webp", + "video/webm" => ".webm", + "video/quicktime" => ".mov", + "video/mp4" => ".mp4", + _ => ".jpg" + }; + + var dir = Path.Combine(_env.ContentRootPath, "uploads", cafeId); + Directory.CreateDirectory(dir); + var fileName = $"{prefix}_{Guid.NewGuid():N}{ext}"; + var path = Path.Combine(dir, fileName); + + await using var stream = File.Create(path); + await file.CopyToAsync(stream, cancellationToken); + + _logger.LogInformation("Saved {Prefix} media for cafe {CafeId}", prefix, cafeId); + return $"/uploads/{cafeId}/{fileName}"; + } +} diff --git a/src/Meezi.API/Services/MenuAi3dGenerationService.cs b/src/Meezi.API/Services/MenuAi3dGenerationService.cs new file mode 100644 index 0000000..571302d --- /dev/null +++ b/src/Meezi.API/Services/MenuAi3dGenerationService.cs @@ -0,0 +1,292 @@ +using System.Net.Http.Headers; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; +using Meezi.API.Configuration; +using Meezi.Core.Constants; +using Meezi.Core.Enums; +using Meezi.Core.Interfaces; +using Meezi.Core.Platform; +using Meezi.Infrastructure.Data; +using Meezi.Infrastructure.Services.Platform; +using StackExchange.Redis; + +namespace Meezi.API.Services; + +public record MenuAi3dUsageDto(int Used, int Limit, string Period); + +public record MenuAi3dGenerateResultDto(string Model3dUrl, int Used, int Limit); + +public interface IMenuAi3dGenerationService +{ + Task GetUsageAsync(string cafeId, PlanTier planTier, CancellationToken cancellationToken = default); + Task<(MenuAi3dGenerateResultDto? Data, string? ErrorCode, string? Message)> GenerateFromItemImageAsync( + string cafeId, + string itemId, + PlanTier planTier, + CancellationToken cancellationToken = default); +} + +public class MenuAi3dGenerationService : IMenuAi3dGenerationService +{ + private const string FeatureMenu3d = "menu_3d"; + private const string FeatureMenu3dAi = "menu_3d_ai"; + + private static readonly JsonSerializerOptions JsonOpts = new() + { + PropertyNameCaseInsensitive = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }; + + private readonly AppDbContext _db; + private readonly IPlatformCatalogService _catalog; + private readonly IMediaStorageService _media; + private readonly IConnectionMultiplexer _redis; + private readonly IHttpClientFactory _httpClientFactory; + private readonly IPlatformRuntimeConfig _platform; + private readonly MenuAi3dOptions _options; + private readonly IConfiguration _configuration; + private readonly IWebHostEnvironment _env; + private readonly ILogger _logger; + + public MenuAi3dGenerationService( + AppDbContext db, + IPlatformCatalogService catalog, + IMediaStorageService media, + IConnectionMultiplexer redis, + IHttpClientFactory httpClientFactory, + IPlatformRuntimeConfig platform, + IOptions options, + IConfiguration configuration, + IWebHostEnvironment env, + ILogger logger) + { + _db = db; + _catalog = catalog; + _media = media; + _redis = redis; + _httpClientFactory = httpClientFactory; + _platform = platform; + _options = options.Value; + _configuration = configuration; + _env = env; + _logger = logger; + } + + public async Task GetUsageAsync( + string cafeId, + PlanTier planTier, + CancellationToken cancellationToken = default) + { + var limit = await ResolveLimitAsync(cafeId, planTier, cancellationToken); + var used = await GetUsedCountAsync(cafeId); + return new MenuAi3dUsageDto(used, limit, DateTime.UtcNow.ToString("yyyy-MM")); + } + + public async Task<(MenuAi3dGenerateResultDto? Data, string? ErrorCode, string? Message)> GenerateFromItemImageAsync( + string cafeId, + string itemId, + PlanTier planTier, + CancellationToken cancellationToken = default) + { + if (!await _catalog.IsFeatureEnabledForCafeAsync(cafeId, planTier, FeatureMenu3d, cancellationToken)) + return (null, "PLAN_FEATURE_DISABLED", "3D menu is not included in your plan."); + + if (!await _catalog.IsFeatureEnabledForCafeAsync(cafeId, planTier, FeatureMenu3dAi, cancellationToken)) + return (null, "PLAN_FEATURE_DISABLED", "AI 3D generation requires Business plan or higher."); + + var limit = await ResolveLimitAsync(cafeId, planTier, cancellationToken); + if (limit <= 0) + return (null, "PLAN_FEATURE_DISABLED", "AI 3D generation is not available on your plan."); + + var used = await GetUsedCountAsync(cafeId); + if (used >= limit) + return (null, "PLAN_LIMIT_REACHED", "Monthly AI 3D generation limit reached (100)."); + + var item = await _db.MenuItems.FirstOrDefaultAsync( + i => i.CafeId == cafeId && i.Id == itemId, + cancellationToken); + if (item is null) + return (null, "NOT_FOUND", "Menu item not found."); + + if (string.IsNullOrWhiteSpace(item.ImageUrl)) + return (null, "NO_IMAGE", "Upload a product photo before generating a 3D model."); + + var imageUrl = ResolvePublicUrl(item.ImageUrl.Trim()); + byte[] glbBytes; + try + { + glbBytes = await GenerateGlbBytesAsync(imageUrl, cancellationToken); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "AI 3D generation failed for cafe {CafeId} item {ItemId}", cafeId, itemId); + return (null, "AI_GENERATION_FAILED", "Could not generate 3D model. Try again later."); + } + + var modelUrl = await _media.SaveMenuModel3dFromBytesAsync(cafeId, glbBytes, cancellationToken); + if (modelUrl is null) + return (null, "INVALID_FILE", "Generated model could not be saved."); + + item.Model3dUrl = modelUrl; + await _db.SaveChangesAsync(cancellationToken); + + var newUsed = await IncrementUsageAsync(cafeId); + return (new MenuAi3dGenerateResultDto(modelUrl, newUsed, limit), null, null); + } + + private async Task ResolveLimitAsync(string cafeId, PlanTier planTier, CancellationToken cancellationToken) + { + if (!await _catalog.IsFeatureEnabledForCafeAsync(cafeId, planTier, FeatureMenu3dAi, cancellationToken)) + return 0; + return PlanLimits.MaxMenuAi3dPerMonth(planTier); + } + + private static string UsageKey(string cafeId) => + $"ai3d:usage:{cafeId}:{DateTime.UtcNow:yyyy-MM}"; + + private async Task GetUsedCountAsync(string cafeId) + { + var redis = _redis.GetDatabase(); + var val = await redis.StringGetAsync(UsageKey(cafeId)); + return val.HasValue && int.TryParse(val.ToString(), out var n) ? n : 0; + } + + private async Task IncrementUsageAsync(string cafeId) + { + var redis = _redis.GetDatabase(); + var key = UsageKey(cafeId); + var next = (int)await redis.StringIncrementAsync(key); + if (next == 1) + { + var endOfMonth = new DateTime(DateTime.UtcNow.Year, DateTime.UtcNow.Month, 1, 0, 0, 0, DateTimeKind.Utc) + .AddMonths(1); + await redis.KeyExpireAsync(key, endOfMonth - DateTime.UtcNow); + } + + return next; + } + + private string ResolvePublicUrl(string url) + { + if (url.StartsWith("http://", StringComparison.OrdinalIgnoreCase) + || url.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) + return url; + + var baseUrl = _configuration["App:PublicBaseUrl"]?.TrimEnd('/') ?? "http://localhost:5080"; + return url.StartsWith('/') ? $"{baseUrl}{url}" : $"{baseUrl}/{url}"; + } + + private async Task GenerateGlbBytesAsync(string imageUrl, CancellationToken cancellationToken) + { + var apiKey = await ResolveMeshyApiKeyAsync(cancellationToken); + if (!string.IsNullOrWhiteSpace(apiKey)) + return await GenerateViaMeshyAsync(imageUrl, apiKey, cancellationToken); + + if (_options.AllowDevStub && _env.IsDevelopment()) + return DevStubGlbBytes(); + + throw new InvalidOperationException("AI 3D provider is not configured."); + } + + private async Task ResolveMeshyApiKeyAsync(CancellationToken cancellationToken) + { + var menu3dOn = await _platform.GetAsync(PlatformIntegrationKeys.MeshyMenu3dEnabled, cancellationToken); + if (menu3dOn is "false") + return null; + + var enabled = await _platform.GetAsync(PlatformIntegrationKeys.MeshyEnabled, cancellationToken); + if (enabled is "false") + return null; + + var fromDb = await _platform.GetAsync(PlatformIntegrationKeys.MeshyApiKey, cancellationToken); + if (!string.IsNullOrWhiteSpace(fromDb)) + return fromDb.Trim(); + + return string.IsNullOrWhiteSpace(_options.ApiKey) ? null : _options.ApiKey.Trim(); + } + + private async Task GenerateViaMeshyAsync( + string imageUrl, + string apiKey, + CancellationToken cancellationToken) + { + var client = _httpClientFactory.CreateClient("MenuAi3d"); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey); + + var baseUrl = _options.BaseUrl.TrimEnd('/'); + var createPath = _options.ImageTo3dPath.TrimStart('/'); + var createBody = JsonSerializer.Serialize(new { image_url = imageUrl }, JsonOpts); + using var createReq = new HttpRequestMessage(HttpMethod.Post, $"{baseUrl}/{createPath}") + { + Content = new StringContent(createBody, Encoding.UTF8, "application/json") + }; + + using var createRes = await client.SendAsync(createReq, cancellationToken); + createRes.EnsureSuccessStatusCode(); + await using var createStream = await createRes.Content.ReadAsStreamAsync(cancellationToken); + var createDoc = await JsonDocument.ParseAsync(createStream, cancellationToken: cancellationToken); + var taskId = createDoc.RootElement.TryGetProperty("result", out var resultEl) + ? resultEl.GetString() + : createDoc.RootElement.TryGetProperty("id", out var idEl) + ? idEl.GetString() + : null; + + if (string.IsNullOrWhiteSpace(taskId)) + throw new InvalidOperationException("AI provider did not return a task id."); + + var pollUrl = $"{baseUrl}/{createPath}/{taskId}"; + var deadline = DateTime.UtcNow.AddSeconds(Math.Max(30, _options.PollTimeoutSeconds)); + var interval = TimeSpan.FromSeconds(Math.Clamp(_options.PollIntervalSeconds, 2, 30)); + + while (DateTime.UtcNow < deadline) + { + await Task.Delay(interval, cancellationToken); + using var pollRes = await client.GetAsync(pollUrl, cancellationToken); + pollRes.EnsureSuccessStatusCode(); + await using var pollStream = await pollRes.Content.ReadAsStreamAsync(cancellationToken); + var pollDoc = await JsonDocument.ParseAsync(pollStream, cancellationToken: cancellationToken); + var status = pollDoc.RootElement.TryGetProperty("status", out var statusEl) + ? statusEl.GetString() + : null; + + if (string.Equals(status, "SUCCEEDED", StringComparison.OrdinalIgnoreCase) + || string.Equals(status, "COMPLETED", StringComparison.OrdinalIgnoreCase)) + { + var glbUrl = ExtractGlbUrl(pollDoc.RootElement); + if (string.IsNullOrWhiteSpace(glbUrl)) + throw new InvalidOperationException("AI provider succeeded but returned no GLB URL."); + + using var downloadRes = await client.GetAsync(glbUrl, cancellationToken); + downloadRes.EnsureSuccessStatusCode(); + return await downloadRes.Content.ReadAsByteArrayAsync(cancellationToken); + } + + if (string.Equals(status, "FAILED", StringComparison.OrdinalIgnoreCase) + || string.Equals(status, "CANCELED", StringComparison.OrdinalIgnoreCase)) + throw new InvalidOperationException("AI provider task failed."); + } + + throw new TimeoutException("AI 3D generation timed out."); + } + + private static string? ExtractGlbUrl(JsonElement root) + { + if (root.TryGetProperty("model_urls", out var urls) + && urls.TryGetProperty("glb", out var glb) + && glb.ValueKind == JsonValueKind.String) + return glb.GetString(); + + if (root.TryGetProperty("model_url", out var single) && single.ValueKind == JsonValueKind.String) + return single.GetString(); + + return null; + } + + /// Minimal valid GLB (empty scene) for local development without Meshy API key. + private static byte[] DevStubGlbBytes() => + Convert.FromBase64String( + "Z2xURgIAAACI3gAQAwEAAFBLQVRGT1JNUwBCeHAEAgAqBUZsAE1BVEhQAgAgAAAAAO4AAABKQwAAAAAAAJAAAAA="); +} diff --git a/src/Meezi.API/Services/MenuService.cs b/src/Meezi.API/Services/MenuService.cs new file mode 100644 index 0000000..452d084 --- /dev/null +++ b/src/Meezi.API/Services/MenuService.cs @@ -0,0 +1,229 @@ +using Meezi.API.Models.Menu; +using Meezi.Core.Constants; +using Meezi.Core.Entities; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; + +namespace Meezi.API.Services; + +public interface IMenuService +{ + Task> GetCategoriesAsync(string cafeId, CancellationToken cancellationToken = default); + Task CreateCategoryAsync(string cafeId, CreateMenuCategoryRequest request, CancellationToken cancellationToken = default); + Task UpdateCategoryAsync(string cafeId, string id, UpdateMenuCategoryRequest request, CancellationToken cancellationToken = default); + Task DeleteCategoryAsync(string cafeId, string id, CancellationToken cancellationToken = default); + Task> GetItemsAsync(string cafeId, string? categoryId, CancellationToken cancellationToken = default); + Task CreateItemAsync(string cafeId, CreateMenuItemRequest request, CancellationToken cancellationToken = default); + Task UpdateItemAsync(string cafeId, string id, UpdateMenuItemRequest request, CancellationToken cancellationToken = default); + Task SetAvailabilityAsync(string cafeId, string id, bool isAvailable, CancellationToken cancellationToken = default); +} + +public class MenuService : IMenuService +{ + private readonly AppDbContext _db; + + public MenuService(AppDbContext db) + { + _db = db; + } + + public async Task> GetCategoriesAsync(string cafeId, CancellationToken cancellationToken = default) + { + return await _db.MenuCategories + .Where(c => c.CafeId == cafeId) + .OrderBy(c => c.SortOrder) + .Select(c => ToCategoryDto(c)) + .ToListAsync(cancellationToken); + } + + public async Task CreateCategoryAsync(string cafeId, CreateMenuCategoryRequest request, CancellationToken cancellationToken = default) + { + var entity = new MenuCategory + { + CafeId = cafeId, + Name = request.Name, + NameAr = request.NameAr, + NameEn = request.NameEn, + SortOrder = request.SortOrder, + TaxId = request.TaxId, + DiscountPercent = request.DiscountPercent, + Icon = NormalizeOptionalText(request.Icon), + IconPresetId = NormalizeIconPreset(request.IconPresetId), + IconStyle = NormalizeIconStyle(request.IconStyle) + ?? (NormalizeIconPreset(request.IconPresetId) is not null + ? CategoryIconPresets.IconStyle.Flat + : null), + ImageUrl = NormalizeOptionalText(request.ImageUrl), + IsActive = request.IsActive, + KitchenStationId = string.IsNullOrWhiteSpace(request.KitchenStationId) + ? null + : request.KitchenStationId + }; + + _db.MenuCategories.Add(entity); + await _db.SaveChangesAsync(cancellationToken); + return ToCategoryDto(entity); + } + + public async Task UpdateCategoryAsync(string cafeId, string id, UpdateMenuCategoryRequest request, CancellationToken cancellationToken = default) + { + var entity = await _db.MenuCategories.FirstOrDefaultAsync(c => c.Id == id && c.CafeId == cafeId, cancellationToken); + if (entity is null) return null; + + if (request.Name is not null) entity.Name = request.Name; + if (request.NameAr is not null) entity.NameAr = request.NameAr; + if (request.NameEn is not null) entity.NameEn = request.NameEn; + if (request.SortOrder.HasValue) entity.SortOrder = request.SortOrder.Value; + if (request.TaxId is not null) entity.TaxId = request.TaxId; + if (request.DiscountPercent.HasValue) entity.DiscountPercent = request.DiscountPercent.Value; + if (request.IsActive.HasValue) entity.IsActive = request.IsActive.Value; + if (request.Icon is not null) + entity.Icon = NormalizeOptionalText(request.Icon); + if (request.IconPresetId is not null) + entity.IconPresetId = NormalizeIconPreset(request.IconPresetId); + if (request.IconStyle is not null) + entity.IconStyle = NormalizeIconStyle(request.IconStyle); + if (request.ImageUrl is not null) + entity.ImageUrl = NormalizeOptionalText(request.ImageUrl); + if (request.KitchenStationId is not null) + entity.KitchenStationId = string.IsNullOrWhiteSpace(request.KitchenStationId) + ? null + : request.KitchenStationId; + + await _db.SaveChangesAsync(cancellationToken); + return ToCategoryDto(entity); + } + + public async Task DeleteCategoryAsync(string cafeId, string id, CancellationToken cancellationToken = default) + { + var entity = await _db.MenuCategories.FirstOrDefaultAsync(c => c.Id == id && c.CafeId == cafeId, cancellationToken); + if (entity is null) return false; + + entity.DeletedAt = DateTime.UtcNow; + await _db.SaveChangesAsync(cancellationToken); + return true; + } + + public async Task> GetItemsAsync(string cafeId, string? categoryId, CancellationToken cancellationToken = default) + { + var query = _db.MenuItems.Where(i => i.CafeId == cafeId); + if (!string.IsNullOrEmpty(categoryId)) + query = query.Where(i => i.CategoryId == categoryId); + + var items = await query.Include(i => i.Category).OrderBy(i => i.Name).ToListAsync(cancellationToken); + return items.Select(ToItemDto).ToList(); + } + + public async Task CreateItemAsync(string cafeId, CreateMenuItemRequest request, CancellationToken cancellationToken = default) + { + var category = await _db.MenuCategories + .FirstOrDefaultAsync(c => c.Id == request.CategoryId && c.CafeId == cafeId, cancellationToken); + if (category is null) return null; + + var imageUrl = string.IsNullOrWhiteSpace(request.ImageUrl) + ? MenuItemImageDefaults.GetDefaultImageUrl( + MenuItemImageDefaults.InferKind(request.CategoryId, category.Name)) + : request.ImageUrl; + + var entity = new MenuItem + { + CafeId = cafeId, + CategoryId = request.CategoryId, + Name = request.Name, + NameAr = request.NameAr, + NameEn = request.NameEn, + Description = request.Description, + Price = request.Price, + DiscountPercent = request.DiscountPercent, + ImageUrl = imageUrl, + VideoUrl = request.VideoUrl, + Model3dUrl = NormalizeOptionalText(request.Model3dUrl), + IsAvailable = request.IsAvailable + }; + + _db.MenuItems.Add(entity); + await _db.SaveChangesAsync(cancellationToken); + return ToItemDto(entity); + } + + public async Task UpdateItemAsync(string cafeId, string id, UpdateMenuItemRequest request, CancellationToken cancellationToken = default) + { + var entity = await _db.MenuItems + .Include(i => i.Category) + .FirstOrDefaultAsync(i => i.Id == id && i.CafeId == cafeId, cancellationToken); + if (entity is null) return null; + + if (request.CategoryId is not null) + { + var categoryExists = await _db.MenuCategories.AnyAsync(c => c.Id == request.CategoryId && c.CafeId == cafeId, cancellationToken); + if (!categoryExists) return null; + entity.CategoryId = request.CategoryId; + } + + if (request.Name is not null) entity.Name = request.Name; + if (request.NameAr is not null) entity.NameAr = request.NameAr; + if (request.NameEn is not null) entity.NameEn = request.NameEn; + if (request.Description is not null) entity.Description = request.Description; + if (request.Price.HasValue) entity.Price = request.Price.Value; + if (request.DiscountPercent.HasValue) entity.DiscountPercent = request.DiscountPercent.Value; + if (request.ImageUrl is not null) + { + entity.ImageUrl = string.IsNullOrWhiteSpace(request.ImageUrl) + ? MenuItemImageDefaults.ResolveImageUrl(entity.Id, entity.CategoryId, entity.Category?.Name) + : request.ImageUrl; + } + if (request.VideoUrl is not null) + entity.VideoUrl = string.IsNullOrWhiteSpace(request.VideoUrl) ? null : request.VideoUrl; + if (request.Model3dUrl is not null) + entity.Model3dUrl = string.IsNullOrWhiteSpace(request.Model3dUrl) ? null : request.Model3dUrl.Trim(); + if (request.IsAvailable.HasValue) entity.IsAvailable = request.IsAvailable.Value; + + await _db.SaveChangesAsync(cancellationToken); + return ToItemDto(entity); + } + + public async Task SetAvailabilityAsync(string cafeId, string id, bool isAvailable, CancellationToken cancellationToken = default) + { + var entity = await _db.MenuItems.FirstOrDefaultAsync(i => i.Id == id && i.CafeId == cafeId, cancellationToken); + if (entity is null) return null; + + entity.IsAvailable = isAvailable; + await _db.SaveChangesAsync(cancellationToken); + return ToItemDto(entity); + } + + private static string? NormalizeOptionalText(string? value) => + string.IsNullOrWhiteSpace(value) ? null : value.Trim(); + + private static string? NormalizeIconPreset(string? value) + { + var normalized = CategoryIconPresets.NormalizePreset(value); + if (normalized is null) return null; + return CategoryIconPresets.IsValidPreset(normalized) ? normalized : null; + } + + private static string? NormalizeIconStyle(string? value) + { + var normalized = CategoryIconPresets.NormalizeStyle(value); + if (normalized is null) return null; + return CategoryIconPresets.IsValidStyle(normalized) ? normalized : null; + } + + private static MenuCategoryDto ToCategoryDto(MenuCategory c) => new( + c.Id, c.Name, c.NameAr, c.NameEn, c.SortOrder, c.TaxId, c.DiscountPercent, + c.Icon, c.IconPresetId, c.IconStyle, c.ImageUrl, c.IsActive, c.KitchenStationId); + + private static MenuItemDto ToItemDto(MenuItem i) => new( + i.Id, + i.CategoryId, + i.Name, + i.NameAr, + i.NameEn, + i.Description, + i.Price, + i.DiscountPercent, + MenuItemImageDefaults.ResolveDisplayImageUrl(i), + i.VideoUrl, + i.Model3dUrl, + i.IsAvailable); +} diff --git a/src/Meezi.API/Services/NotificationInboxService.cs b/src/Meezi.API/Services/NotificationInboxService.cs new file mode 100644 index 0000000..dc0eeb7 --- /dev/null +++ b/src/Meezi.API/Services/NotificationInboxService.cs @@ -0,0 +1,80 @@ +using Meezi.API.Models.Notifications; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; + +namespace Meezi.API.Services; + +public interface INotificationInboxService +{ + Task ListAsync( + string cafeId, + bool unreadOnly, + int limit, + CancellationToken ct = default); + + Task GetUnreadCountAsync(string cafeId, CancellationToken ct = default); + + Task MarkReadAsync(string cafeId, MarkNotificationsReadRequest request, CancellationToken ct = default); +} + +public class NotificationInboxService : INotificationInboxService +{ + private readonly AppDbContext _db; + + public NotificationInboxService(AppDbContext db) + { + _db = db; + } + + public async Task ListAsync( + string cafeId, + bool unreadOnly, + int limit, + CancellationToken ct = default) + { + limit = Math.Clamp(limit, 1, 100); + var q = _db.CafeNotifications.AsNoTracking().Where(n => n.CafeId == cafeId); + if (unreadOnly) + q = q.Where(n => !n.IsRead); + + var unread = await q.CountAsync(n => !n.IsRead, ct); + var items = await q + .OrderByDescending(n => n.CreatedAt) + .Take(limit) + .Select(n => new CafeNotificationDto( + n.Id, + n.Type, + n.Title, + n.Body, + n.ReferenceId, + n.TableNumber, + n.IsRead, + n.CreatedAt)) + .ToListAsync(ct); + + return new NotificationListDto(items, unread); + } + + public Task GetUnreadCountAsync(string cafeId, CancellationToken ct = default) => + _db.CafeNotifications.CountAsync(n => n.CafeId == cafeId && !n.IsRead, ct); + + public async Task MarkReadAsync( + string cafeId, + MarkNotificationsReadRequest request, + CancellationToken ct = default) + { + var q = _db.CafeNotifications.Where(n => n.CafeId == cafeId && !n.IsRead); + if (!request.All && request.Ids is { Count: > 0 }) + q = q.Where(n => request.Ids.Contains(n.Id)); + + var rows = await q.ToListAsync(ct); + var now = DateTime.UtcNow; + foreach (var row in rows) + { + row.IsRead = true; + row.ReadAt = now; + } + + await _db.SaveChangesAsync(ct); + } +} diff --git a/src/Meezi.API/Services/OpenAiChatService.cs b/src/Meezi.API/Services/OpenAiChatService.cs new file mode 100644 index 0000000..fa39bd9 --- /dev/null +++ b/src/Meezi.API/Services/OpenAiChatService.cs @@ -0,0 +1,117 @@ +using System.Net.Http.Headers; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using Meezi.Core.Interfaces; +using Meezi.Core.Platform; + +namespace Meezi.API.Services; + +public interface IOpenAiChatService +{ + Task IsConfiguredForCoffeeAdvisorAsync(CancellationToken cancellationToken = default); + Task CompleteJsonAsync(string systemPrompt, string userPrompt, CancellationToken cancellationToken = default); +} + +public class OpenAiChatService : IOpenAiChatService +{ + private static readonly JsonSerializerOptions JsonOpts = new() + { + PropertyNameCaseInsensitive = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }; + + private readonly IHttpClientFactory _httpClientFactory; + private readonly IPlatformRuntimeConfig _platform; + private readonly IConfiguration _configuration; + private readonly ILogger _logger; + + public OpenAiChatService( + IHttpClientFactory httpClientFactory, + IPlatformRuntimeConfig platform, + IConfiguration configuration, + ILogger logger) + { + _httpClientFactory = httpClientFactory; + _platform = platform; + _configuration = configuration; + _logger = logger; + } + + public async Task IsConfiguredForCoffeeAdvisorAsync(CancellationToken cancellationToken = default) + { + if (!await IsCoffeeAdvisorEnabledAsync(cancellationToken)) + return false; + return !string.IsNullOrWhiteSpace(await GetApiKeyAsync(cancellationToken)); + } + + public async Task CompleteJsonAsync( + string systemPrompt, + string userPrompt, + CancellationToken cancellationToken = default) + { + var apiKey = await GetApiKeyAsync(cancellationToken); + if (string.IsNullOrWhiteSpace(apiKey)) + return null; + + var model = await GetModelAsync(cancellationToken); + var body = JsonSerializer.Serialize(new + { + model, + temperature = 0.4, + response_format = new { type = "json_object" }, + messages = new object[] + { + new { role = "system", content = systemPrompt }, + new { role = "user", content = userPrompt } + } + }, JsonOpts); + + var client = _httpClientFactory.CreateClient("OpenAi"); + using var req = new HttpRequestMessage(HttpMethod.Post, "https://api.openai.com/v1/chat/completions") + { + Content = new StringContent(body, Encoding.UTF8, "application/json") + }; + req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey); + + using var res = await client.SendAsync(req, cancellationToken); + if (!res.IsSuccessStatusCode) + { + _logger.LogWarning("OpenAI chat failed with status {Status}", res.StatusCode); + return null; + } + + await using var stream = await res.Content.ReadAsStreamAsync(cancellationToken); + var doc = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken); + return doc.RootElement + .GetProperty("choices")[0] + .GetProperty("message") + .GetProperty("content") + .GetString(); + } + + private async Task IsCoffeeAdvisorEnabledAsync(CancellationToken cancellationToken) + { + var enabled = await _platform.GetAsync(PlatformIntegrationKeys.OpenAiEnabled, cancellationToken); + if (enabled is "false") + return false; + var feature = await _platform.GetAsync(PlatformIntegrationKeys.OpenAiCoffeeAdvisorEnabled, cancellationToken); + return feature is not "false"; + } + + private async Task GetApiKeyAsync(CancellationToken cancellationToken) + { + var fromDb = await _platform.GetAsync(PlatformIntegrationKeys.OpenAiApiKey, cancellationToken); + if (!string.IsNullOrWhiteSpace(fromDb)) + return fromDb.Trim(); + return _configuration["OpenAI:ApiKey"]?.Trim(); + } + + private async Task GetModelAsync(CancellationToken cancellationToken) + { + var fromDb = await _platform.GetAsync(PlatformIntegrationKeys.OpenAiModel, cancellationToken); + if (!string.IsNullOrWhiteSpace(fromDb)) + return fromDb.Trim(); + return _configuration["OpenAI:Model"]?.Trim() ?? "gpt-4o-mini"; + } +} diff --git a/src/Meezi.API/Services/OrderNotificationService.cs b/src/Meezi.API/Services/OrderNotificationService.cs new file mode 100644 index 0000000..73c9c64 --- /dev/null +++ b/src/Meezi.API/Services/OrderNotificationService.cs @@ -0,0 +1,162 @@ +using Microsoft.AspNetCore.SignalR; +using Microsoft.EntityFrameworkCore; +using Meezi.API.Hubs; +using Meezi.API.Models.Notifications; +using Meezi.API.Models.Orders; +using Meezi.API.Models.Public; +using Meezi.API.Services.Printing; +using Meezi.Core.Entities; +using Meezi.Core.Enums; +using Meezi.Core.Interfaces; +using Meezi.Infrastructure.Data; + +namespace Meezi.API.Services; + +public class OrderNotificationService : IOrderNotificationService +{ + private readonly AppDbContext _db; + private readonly IHubContext _kdsHub; + private readonly IHubContext _guestHub; + private readonly ISmsService _sms; + private readonly ILogger _logger; + + public OrderNotificationService( + AppDbContext db, + IHubContext kdsHub, + IHubContext guestHub, + ISmsService sms, + ILogger logger) + { + _db = db; + _kdsHub = kdsHub; + _guestHub = guestHub; + _sms = sms; + _logger = logger; + } + + public async Task NotifyGuestOrderPlacedAsync(Order order, LiveOrderDto live, CancellationToken ct = default) + { + if (order.Source != OrderSource.GuestQr) + return; + + var tableNumber = await ResolveTableNumberAsync(order, ct); + var orderNumber = ReceiptPrintFormatting.OrderNumberLabel(order.DisplayNumber > 0 ? order.DisplayNumber : ReceiptPrintFormatting.StableDisplayNumberFromId(order.Id)); + var notification = new CafeNotification + { + CafeId = order.CafeId, + Type = "guest_order_new", + Title = $"سفارش جدید میز {tableNumber ?? "—"}", + Body = $"شماره {orderNumber} · {order.Total:N0} ت", + ReferenceId = order.Id, + TableNumber = tableNumber + }; + _db.CafeNotifications.Add(notification); + await _db.SaveChangesAsync(ct); + + var dto = MapNotification(notification); + await _kdsHub.Clients.Group(KdsHub.GroupName(order.CafeId)) + .SendAsync("NotificationReceived", dto, ct); + + await PushGuestTrackAsync(order, ct); + } + + public async Task NotifyOrderStatusChangedAsync(Order order, CancellationToken ct = default) + { + await PushGuestTrackAsync(order, ct); + + if (order.Source != OrderSource.GuestQr) + return; + + if (order.Status == OrderStatus.Ready) + { + var tableNumber = await ResolveTableNumberAsync(order, ct); + var orderNumber = ReceiptPrintFormatting.OrderNumberLabel(order.DisplayNumber > 0 ? order.DisplayNumber : ReceiptPrintFormatting.StableDisplayNumberFromId(order.Id)); + var readyNotification = new CafeNotification + { + CafeId = order.CafeId, + Type = "guest_order_ready", + Title = $"سفارش آماده — میز {tableNumber ?? "—"}", + Body = $"شماره {orderNumber}", + ReferenceId = order.Id, + TableNumber = tableNumber + }; + _db.CafeNotifications.Add(readyNotification); + await _db.SaveChangesAsync(ct); + await _kdsHub.Clients.Group(KdsHub.GroupName(order.CafeId)) + .SendAsync("NotificationReceived", MapNotification(readyNotification), ct); + + await TrySmsGuestReadyAsync(order, tableNumber, orderNumber, ct); + } + } + + private async Task PushGuestTrackAsync(Order order, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(order.GuestTrackingToken)) + return; + + var full = await _db.Orders + .Include(o => o.Items) + .ThenInclude(i => i.MenuItem) + .Include(o => o.Table) + .FirstOrDefaultAsync(o => o.Id == order.Id, cancellationToken); + if (full is null) return; + + var track = OrderTrackingHelper.BuildTrackDto(full); + await _guestHub.Clients.Group(GuestOrderHub.OrderGroup(order.Id)) + .SendAsync("OrderTrackUpdated", track, cancellationToken); + } + + private async Task TrySmsGuestReadyAsync( + Order order, + string? tableNumber, + string orderNumber, + CancellationToken ct) + { + var phone = order.GuestPhone?.Trim(); + if (string.IsNullOrEmpty(phone)) + return; + + try + { + var msg = $"میزی: سفارش {orderNumber} (میز {tableNumber ?? "—"}) آماده است."; + await _sms.SendMessageAsync(phone, msg, ct); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Guest ready SMS failed for order {OrderId}", order.Id); + } + } + + private async Task ResolveTableNumberAsync(Order order, CancellationToken cancellationToken) + { + if (order.Table is not null) + return order.Table.Number; + if (string.IsNullOrEmpty(order.TableId)) + return null; + return await _db.Tables + .Where(t => t.Id == order.TableId) + .Select(t => t.Number) + .FirstOrDefaultAsync(cancellationToken); + } + + public async Task NotifyCallWaiterAsync(string cafeId, string tableId, string tableNumber, CancellationToken ct = default) + { + var notification = new CafeNotification + { + CafeId = cafeId, + Type = "table_call_waiter", + Title = $"درخواست گارسون — میز {tableNumber}", + Body = "مشتری درخواست خدمت کرد", + ReferenceId = tableId, + TableNumber = tableNumber + }; + _db.CafeNotifications.Add(notification); + await _db.SaveChangesAsync(ct); + + await _kdsHub.Clients.Group(KdsHub.GroupName(cafeId)) + .SendAsync("NotificationReceived", MapNotification(notification), ct); + } + + private static CafeNotificationDto MapNotification(CafeNotification n) => + new(n.Id, n.Type, n.Title, n.Body, n.ReferenceId, n.TableNumber, n.IsRead, n.CreatedAt); +} diff --git a/src/Meezi.API/Services/OrderService.cs b/src/Meezi.API/Services/OrderService.cs new file mode 100644 index 0000000..c0ceca3 --- /dev/null +++ b/src/Meezi.API/Services/OrderService.cs @@ -0,0 +1,1172 @@ +using Meezi.API.Models.Orders; +using Meezi.API.Models.Public; +using Meezi.API.Services.Delivery; +using Meezi.API.Services.Printing; +using Meezi.Core.Entities; +using Meezi.Core.Enums; +using Meezi.Core.Utilities; +using Meezi.Core.Interfaces; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage; + +namespace Meezi.API.Services; + +public record OrderServiceResult(bool Success, T? Data, string? ErrorCode = null, string? Field = null); + +public interface IOrderService +{ + Task> GetOrdersAsync(string cafeId, OrderStatus? status, CancellationToken cancellationToken = default); + Task> GetOpenOrdersAsync(string cafeId, string? search, CancellationToken cancellationToken = default); + Task> GetLiveOrdersAsync(string cafeId, CancellationToken cancellationToken = default); + Task GetOrderAsync(string cafeId, string orderId, CancellationToken cancellationToken = default); + Task GetActiveOrderByTableAsync(string cafeId, string tableId, CancellationToken cancellationToken = default); + Task> CreateOrderAsync(string cafeId, ITenantContext tenant, CreateOrderRequest request, CancellationToken cancellationToken = default); + Task CreateGuestOrderAsync( + string cafeId, + CreateOrderRequest request, + string? guestPhone, + string? guestName, + CancellationToken cancellationToken = default); + Task<(GuestQrOrderPlacedDto? Data, string? ErrorCode, string? Message)> PlaceBranchGuestOrderAsync( + string cafeId, + string branchId, + PlaceGuestOrderRequest request, + CancellationToken cancellationToken = default); + Task> AppendOrderItemsAsync( + string cafeId, + string orderId, + AppendOrderItemsRequest request, + CancellationToken cancellationToken = default); + Task> UpdateOrderSessionAsync( + string cafeId, + string orderId, + UpdateOrderSessionRequest request, + CancellationToken cancellationToken = default); + Task> VoidOrderItemAsync( + string cafeId, + string orderId, + string itemId, + string voidedByUserId, + CancellationToken cancellationToken = default); + Task> TransferTableAsync( + string cafeId, + string orderId, + string targetTableId, + CancellationToken cancellationToken = default); + Task UpdateStatusAsync(string cafeId, string orderId, OrderStatus status, CancellationToken cancellationToken = default); + Task>> RecordPaymentsAsync( + string cafeId, + string orderId, + RecordPaymentsRequest request, + string? userId, + CancellationToken cancellationToken = default); +} + +public class OrderService : IOrderService +{ + public static readonly OrderStatus[] LiveStatuses = + [ + OrderStatus.Pending, + OrderStatus.Confirmed, + OrderStatus.Preparing, + OrderStatus.Ready + ]; + + public static readonly OrderStatus[] OpenForPaymentStatuses = + [ + OrderStatus.Pending, + OrderStatus.Confirmed, + OrderStatus.Preparing, + OrderStatus.Ready + ]; + + private readonly AppDbContext _db; + private readonly IKdsNotifier _kdsNotifier; + private readonly ISnappfoodClient _snappfood; + private readonly IDeliveryStatusSyncService _deliverySync; + private readonly ILoyaltyService _loyalty; + private readonly IShiftService _shiftService; + private readonly IServiceScopeFactory _scopeFactory; + private readonly IOrderNotificationService _orderNotifications; + private readonly IInventoryService _inventory; + + public OrderService( + AppDbContext db, + IKdsNotifier kdsNotifier, + ISnappfoodClient snappfood, + IDeliveryStatusSyncService deliverySync, + IShiftService shiftService, + IServiceScopeFactory scopeFactory, + IOrderNotificationService orderNotifications, + IInventoryService inventory, + ILoyaltyService loyalty) + { + _db = db; + _kdsNotifier = kdsNotifier; + _snappfood = snappfood; + _deliverySync = deliverySync; + _shiftService = shiftService; + _scopeFactory = scopeFactory; + _orderNotifications = orderNotifications; + _inventory = inventory; + _loyalty = loyalty; + } + + public async Task> GetOrdersAsync(string cafeId, OrderStatus? status, CancellationToken cancellationToken = default) + { + var query = ApplyOrderIncludes(_db.Orders.Where(o => o.CafeId == cafeId)); + + if (status.HasValue) + query = query.Where(o => o.Status == status.Value); + + var orders = await query.OrderByDescending(o => o.CreatedAt).ToListAsync(cancellationToken); + return orders.Select(MapOrder).ToList(); + } + + public async Task> GetOpenOrdersAsync( + string cafeId, + string? search, + CancellationToken cancellationToken = default) + { + var query = ApplyOrderIncludes(_db.Orders.Where(o => o.CafeId == cafeId)) + .Where(o => OpenForPaymentStatuses.Contains(o.Status)); + + if (!string.IsNullOrWhiteSpace(search)) + { + var term = search.Trim(); + query = query.Where(o => + o.Id.Contains(term) + || (o.GuestName != null && o.GuestName.Contains(term)) + || (o.GuestPhone != null && o.GuestPhone.Contains(term)) + || (o.CustomerId != null && o.CustomerId == term) + || (o.Customer != null && o.Customer.Phone.Contains(term)) + || (o.Table != null && o.Table.Number.Contains(term))); + } + + var orders = await query.OrderByDescending(o => o.CreatedAt).ToListAsync(cancellationToken); + return orders.Select(MapOrder).ToList(); + } + + public async Task> GetLiveOrdersAsync(string cafeId, CancellationToken cancellationToken = default) + { + var orders = await _db.Orders + .Include(o => o.Items) + .ThenInclude(i => i.MenuItem) + .Include(o => o.Table) + .Where(o => o.CafeId == cafeId && LiveStatuses.Contains(o.Status)) + .OrderBy(o => o.CreatedAt) + .ToListAsync(cancellationToken); + + return orders.Select(MapLiveOrder).ToList(); + } + + public async Task GetOrderAsync(string cafeId, string orderId, CancellationToken cancellationToken = default) + { + var order = await LoadOrderAsync(cafeId, orderId, cancellationToken); + return order is null ? null : MapOrder(order); + } + + public async Task GetActiveOrderByTableAsync( + string cafeId, + string tableId, + CancellationToken cancellationToken = default) + { + var order = await FindOpenOrderForTableAsync(cafeId, tableId, cancellationToken); + return order is null ? null : MapOrder(order); + } + + public async Task CreateGuestOrderAsync( + string cafeId, + CreateOrderRequest request, + string? guestPhone, + string? guestName, + CancellationToken cancellationToken = default) + { + var result = await CreateOrderCoreAsync( + cafeId, + request, + employeeId: null, + guestPhone, + guestName, + source: OrderSource.GuestQr, + cancellationToken: cancellationToken); + return result.Data; + } + + public async Task<(GuestQrOrderPlacedDto? Data, string? ErrorCode, string? Message)> PlaceBranchGuestOrderAsync( + string cafeId, + string branchId, + PlaceGuestOrderRequest request, + CancellationToken cancellationToken = default) + { + if (request.Items.Count == 0) + return (null, "VALIDATION_ERROR", "Order must include at least one item."); + + var table = await _db.Tables.FirstOrDefaultAsync( + t => t.Id == request.TableId && t.CafeId == cafeId && t.BranchId == branchId && t.IsActive, + cancellationToken); + + if (table is null) + return (null, "TABLE_NOT_FOUND", "Table not found."); + + if (table.IsCleaning) + return (null, "TABLE_CLEANING", "Table is being cleaned."); + + var branchOk = await _db.Branches.AnyAsync( + b => b.Id == branchId && b.CafeId == cafeId && b.IsActive, + cancellationToken); + if (!branchOk) + return (null, "BRANCH_NOT_FOUND", "Branch not found."); + + var menuItemIds = request.Items.Select(i => i.MenuItemId).Distinct().ToList(); + var menuItems = await _db.MenuItems + .Where(m => m.CafeId == cafeId && menuItemIds.Contains(m.Id) && m.IsAvailable) + .ToListAsync(cancellationToken); + + if (menuItems.Count != menuItemIds.Count) + return (null, "INVALID_MENU_ITEMS", "One or more menu items are invalid."); + + var overrides = await _db.BranchMenuItemOverrides + .Where(o => o.CafeId == cafeId && o.BranchId == branchId && menuItemIds.Contains(o.MenuItemId)) + .ToDictionaryAsync(o => o.MenuItemId, cancellationToken); + + foreach (var item in menuItems) + { + if (overrides.TryGetValue(item.Id, out var ov) && !ov.IsAvailable) + return (null, "INVALID_MENU_ITEMS", "Item is not available at this branch."); + } + + var priceByItem = menuItems.ToDictionary( + m => m.Id, + m => + { + overrides.TryGetValue(m.Id, out var ov); + return ov?.PriceOverride ?? m.Price; + }); + + var createRequest = new CreateOrderRequest( + OrderType.DineIn, + branchId, + request.TableId, + null, + request.GuestName, + request.GuestPhone, + null, + null, + request.Items); + + var result = await CreateOrderCoreAsync( + cafeId, + createRequest, + employeeId: null, + request.GuestPhone, + request.GuestName, + source: OrderSource.GuestQr, + unitPriceByMenuItemId: priceByItem, + cancellationToken: cancellationToken); + + if (!result.Success || result.Data is null) + return (null, result.ErrorCode ?? "ERROR", "Could not place order."); + + var itemCount = request.Items.Sum(i => i.Quantity); + var orderNumber = ReceiptPrintFormatting.OrderNumberLabel(result.Data.DisplayNumber); + var orderEntity = await _db.Orders + .FirstOrDefaultAsync(o => o.Id == result.Data.Id && o.CafeId == cafeId, cancellationToken); + if (orderEntity is null) + return (null, "ORDER_NOT_FOUND", "Could not load placed order."); + + if (string.IsNullOrEmpty(orderEntity.GuestTrackingToken)) + { + orderEntity.GuestTrackingToken = OrderTrackingHelper.NewTrackingToken(); + orderEntity.StatusUpdatedAt = DateTime.UtcNow; + await _db.SaveChangesAsync(cancellationToken); + } + + return ( + new GuestQrOrderPlacedDto( + result.Data.Id, + orderNumber, + result.Data.Total, + itemCount, + result.Data.Status, + orderEntity.GuestTrackingToken), + null, + null); + } + + public async Task> CreateOrderAsync( + string cafeId, + ITenantContext tenant, + CreateOrderRequest request, + CancellationToken cancellationToken = default) => + await CreateOrderCoreAsync( + cafeId, + request, + tenant.UserId, + request.GuestPhone, + request.GuestName, + cancellationToken: cancellationToken); + + public async Task> AppendOrderItemsAsync( + string cafeId, + string orderId, + AppendOrderItemsRequest request, + CancellationToken cancellationToken = default) + { + await using var tx = await BeginTransactionIfSupportedAsync(cancellationToken); + + var order = await ApplyOrderIncludes(_db.Orders) + .FirstOrDefaultAsync(o => o.Id == orderId && o.CafeId == cafeId, cancellationToken); + + if (order is null) + return new OrderServiceResult(false, null, "ORDER_NOT_FOUND"); + + if (!OpenForPaymentStatuses.Contains(order.Status)) + return new OrderServiceResult(false, null, "ORDER_NOT_OPEN"); + + var menuItemIds = request.Items.Select(i => i.MenuItemId).Distinct().ToList(); + var menuItems = await _db.MenuItems + .Where(m => m.CafeId == cafeId && menuItemIds.Contains(m.Id) && m.IsAvailable) + .ToListAsync(cancellationToken); + + if (menuItems.Count != menuItemIds.Count) + return new OrderServiceResult(false, null, "INVALID_ORDER"); + + foreach (var line in request.Items) + { + var menuItem = menuItems.First(m => m.Id == line.MenuItemId); + order.Items.Add(new OrderItem + { + OrderId = order.Id, + MenuItemId = menuItem.Id, + Quantity = line.Quantity, + UnitPrice = menuItem.Price, + Notes = line.Notes + }); + } + + await RecalculateOrderTotalsAsync(order, cafeId, cancellationToken); + await _db.SaveChangesAsync(cancellationToken); + if (tx is not null) + await tx.CommitAsync(cancellationToken); + + var loaded = await LoadOrderAsync(cafeId, order.Id, cancellationToken); + if (loaded is not null) + { + await _kdsNotifier.NotifyOrderStatusChangedAsync(cafeId, order.Id, loaded.Status, cancellationToken); + if (!string.IsNullOrEmpty(loaded.TableId)) + await _kdsNotifier.NotifyTableStatusChangedAsync(cafeId, cancellationToken); + PrinterBackgroundJobs.QueueKitchenPrint(_scopeFactory, cafeId, order.Id); + } + + return loaded is null + ? new OrderServiceResult(false, null, "ORDER_NOT_FOUND") + : new OrderServiceResult(true, MapOrder(loaded)); + } + + public async Task> UpdateOrderSessionAsync( + string cafeId, + string orderId, + UpdateOrderSessionRequest request, + CancellationToken cancellationToken = default) + { + var order = await _db.Orders + .FirstOrDefaultAsync(o => o.Id == orderId && o.CafeId == cafeId, cancellationToken); + + if (order is null) + return new OrderServiceResult(false, null, "ORDER_NOT_FOUND"); + + if (!OpenForPaymentStatuses.Contains(order.Status)) + return new OrderServiceResult(false, null, "ORDER_NOT_OPEN"); + + if (request.GuestName is not null) + order.GuestName = string.IsNullOrWhiteSpace(request.GuestName) ? null : request.GuestName.Trim(); + + if (request.GuestPhone is not null) + order.GuestPhone = NormalizeGuestPhone(request.GuestPhone); + + if (request.CustomerId is not null) + { + if (string.IsNullOrEmpty(request.CustomerId)) + { + order.CustomerId = null; + } + else + { + var exists = await _db.Customers.AnyAsync( + c => c.Id == request.CustomerId && c.CafeId == cafeId, + cancellationToken); + if (!exists) + return new OrderServiceResult(false, null, "INVALID_ORDER", "customerId"); + order.CustomerId = request.CustomerId; + } + } + + if (!string.IsNullOrEmpty(order.GuestPhone) && string.IsNullOrEmpty(order.CustomerId)) + order.CustomerId = await ResolveCustomerIdFromPhoneAsync( + cafeId, + order.GuestPhone, + order.GuestName, + cancellationToken); + + await _db.SaveChangesAsync(cancellationToken); + + var loaded = await LoadOrderAsync(cafeId, orderId, cancellationToken); + return loaded is null + ? new OrderServiceResult(false, null, "ORDER_NOT_FOUND") + : new OrderServiceResult(true, MapOrder(loaded)); + } + + public async Task> VoidOrderItemAsync( + string cafeId, + string orderId, + string itemId, + string voidedByUserId, + CancellationToken cancellationToken = default) + { + var order = await _db.Orders + .Include(o => o.Items) + .FirstOrDefaultAsync(o => o.Id == orderId && o.CafeId == cafeId, cancellationToken); + + if (order is null) + return new OrderServiceResult(false, null, "ORDER_NOT_FOUND"); + + if (order.Status is OrderStatus.Delivered or OrderStatus.Cancelled) + return new OrderServiceResult(false, null, "ORDER_ALREADY_CLOSED"); + + if (!OpenForPaymentStatuses.Contains(order.Status)) + return new OrderServiceResult(false, null, "ORDER_NOT_OPEN"); + + var item = order.Items.FirstOrDefault(i => i.Id == itemId); + if (item is null) + return new OrderServiceResult(false, null, "ITEM_NOT_FOUND"); + + if (item.IsVoided) + return new OrderServiceResult(false, null, "ITEM_ALREADY_VOIDED"); + + item.IsVoided = true; + item.VoidedAt = DateTime.UtcNow; + item.VoidedByUserId = voidedByUserId; + + await RecalculateOrderTotalsAsync(order, cafeId, cancellationToken); + await _db.SaveChangesAsync(cancellationToken); + + var loaded = await LoadOrderAsync(cafeId, orderId, cancellationToken); + if (loaded is not null && !string.IsNullOrEmpty(loaded.TableId)) + await _kdsNotifier.NotifyTableStatusChangedAsync(cafeId, cancellationToken); + + return loaded is null + ? new OrderServiceResult(false, null, "ORDER_NOT_FOUND") + : new OrderServiceResult(true, MapOrder(loaded)); + } + + public async Task> TransferTableAsync( + string cafeId, + string orderId, + string targetTableId, + CancellationToken cancellationToken = default) + { + var order = await _db.Orders + .FirstOrDefaultAsync(o => o.Id == orderId && o.CafeId == cafeId, cancellationToken); + + if (order is null) + return new OrderServiceResult(false, null, "ORDER_NOT_FOUND"); + + if (order.Status is OrderStatus.Delivered or OrderStatus.Cancelled) + return new OrderServiceResult(false, null, "ORDER_ALREADY_CLOSED"); + + if (!OpenForPaymentStatuses.Contains(order.Status)) + return new OrderServiceResult(false, null, "ORDER_NOT_OPEN"); + + var targetTable = await _db.Tables + .FirstOrDefaultAsync(t => t.Id == targetTableId && t.CafeId == cafeId, cancellationToken); + + if (targetTable is null) + return new OrderServiceResult(false, null, "TABLE_NOT_FOUND"); + + if (targetTable.IsCleaning) + return new OrderServiceResult(false, null, "TABLE_CLEANING", "targetTableId"); + + var targetOccupied = await _db.Orders.AnyAsync( + o => o.TableId == targetTableId + && o.CafeId == cafeId + && OpenForPaymentStatuses.Contains(o.Status) + && o.Id != orderId, + cancellationToken); + + if (targetOccupied) + return new OrderServiceResult(false, null, "TABLE_OCCUPIED", "targetTableId"); + + order.TableId = targetTableId; + await _db.SaveChangesAsync(cancellationToken); + + await _kdsNotifier.NotifyTableStatusChangedAsync(cafeId, cancellationToken); + + var loaded = await LoadOrderAsync(cafeId, orderId, cancellationToken); + return loaded is null + ? new OrderServiceResult(false, null, "ORDER_NOT_FOUND") + : new OrderServiceResult(true, MapOrder(loaded)); + } + + private async Task> CreateOrderCoreAsync( + string cafeId, + CreateOrderRequest request, + string? employeeId, + string? guestPhone, + string? guestName, + OrderSource source = OrderSource.Pos, + IReadOnlyDictionary? unitPriceByMenuItemId = null, + CancellationToken cancellationToken = default) + { + if (request.Items.Count == 0) + return new OrderServiceResult(false, null, "INVALID_ORDER"); + + await using var tx = await BeginTransactionIfSupportedAsync(cancellationToken); + + var tableId = request.TableId; + TableReservation? reservation = null; + + if (!string.IsNullOrEmpty(request.ReservationId)) + { + reservation = await _db.TableReservations + .Include(r => r.Table) + .FirstOrDefaultAsync( + r => r.Id == request.ReservationId && r.CafeId == cafeId, + cancellationToken); + + if (reservation is null + || reservation.Status is ReservationStatus.Cancelled + || reservation.Status is ReservationStatus.Completed) + return new OrderServiceResult(false, null, "INVALID_ORDER"); + + if (string.IsNullOrEmpty(tableId) && !string.IsNullOrEmpty(reservation.TableId)) + tableId = reservation.TableId; + + if (string.IsNullOrWhiteSpace(guestPhone)) + guestPhone = reservation.GuestPhone; + if (string.IsNullOrWhiteSpace(guestName)) + guestName = reservation.GuestName; + + reservation.Status = ReservationStatus.Seated; + } + + if (!string.IsNullOrEmpty(tableId)) + { + var table = await _db.Tables.FirstOrDefaultAsync( + t => t.Id == tableId && t.CafeId == cafeId, + cancellationToken); + + if (table is null) + return new OrderServiceResult(false, null, "INVALID_ORDER", "tableId"); + + if (table.IsCleaning) + return new OrderServiceResult(false, null, "TABLE_NOT_AVAILABLE", "tableId"); + + var existing = await FindOpenOrderForTableAsync(cafeId, tableId, cancellationToken); + if (existing is not null) + { + if (source == OrderSource.GuestQr) + existing.Source = OrderSource.GuestQr; + await AppendLinesToOrderAsync( + existing, + request.Items, + cafeId, + unitPriceByMenuItemId, + cancellationToken); + ApplyGuestFields(existing, request, guestPhone, guestName); + if (!string.IsNullOrEmpty(request.CustomerId)) + existing.CustomerId = request.CustomerId; + else if (!string.IsNullOrWhiteSpace(guestPhone)) + existing.CustomerId = await ResolveCustomerIdFromPhoneAsync( + cafeId, + guestPhone.Trim(), + existing.GuestName ?? guestName, + cancellationToken); + + await RecalculateOrderTotalsAsync(existing, cafeId, cancellationToken); + await _db.SaveChangesAsync(cancellationToken); + + await _inventory.DeductForOrderAsync( + cafeId, + existing.Id, + request.Items.Select(i => (i.MenuItemId, i.Quantity)).ToList(), + cancellationToken); + + if (tx is not null) + await tx.CommitAsync(cancellationToken); + + var merged = await LoadOrderAsync(cafeId, existing.Id, cancellationToken); + if (merged is not null) + { + if (source == OrderSource.GuestQr && string.IsNullOrEmpty(merged.GuestTrackingToken)) + { + merged.GuestTrackingToken = OrderTrackingHelper.NewTrackingToken(); + merged.StatusUpdatedAt = DateTime.UtcNow; + await _db.SaveChangesAsync(cancellationToken); + } + + await _kdsNotifier.NotifyOrderStatusChangedAsync(cafeId, merged.Id, merged.Status, cancellationToken); + await _kdsNotifier.NotifyTableStatusChangedAsync(cafeId, cancellationToken); + if (source == OrderSource.GuestQr) + { + var entity = await LoadOrderAsync(cafeId, merged.Id, cancellationToken); + if (entity is not null) + { + await _orderNotifications.NotifyGuestOrderPlacedAsync( + entity, MapLiveOrder(entity), cancellationToken); + } + } + } + + return merged is null + ? new OrderServiceResult(false, null, "ORDER_NOT_FOUND") + : new OrderServiceResult(true, MapOrder(merged)); + } + } + + var createResult = await CreateNewOrderAsync( + cafeId, + request, + employeeId, + guestPhone, + guestName, + tableId, + reservation, + source, + unitPriceByMenuItemId, + cancellationToken); + + if (!createResult.Success) + return createResult; + + if (tx is not null) + await tx.CommitAsync(cancellationToken); + return createResult; + } + + private async Task BeginTransactionIfSupportedAsync(CancellationToken cancellationToken) + { + if (!_db.Database.IsRelational()) + return null; + return await _db.Database.BeginTransactionAsync(cancellationToken); + } + + private async Task> CreateNewOrderAsync( + string cafeId, + CreateOrderRequest request, + string? employeeId, + string? guestPhone, + string? guestName, + string? tableId, + TableReservation? reservation, + OrderSource source, + IReadOnlyDictionary? unitPriceByMenuItemId, + CancellationToken cancellationToken) + { + var menuItemIds = request.Items.Select(i => i.MenuItemId).Distinct().ToList(); + var menuItems = await _db.MenuItems + .Include(m => m.Category) + .Where(m => m.CafeId == cafeId && menuItemIds.Contains(m.Id) && m.IsAvailable) + .ToListAsync(cancellationToken); + + if (menuItems.Count != menuItemIds.Count) + return new OrderServiceResult(false, null, "INVALID_ORDER"); + + string? orderBranchId = request.BranchId; + if (!string.IsNullOrEmpty(orderBranchId)) + { + var branchOk = await _db.Branches.AnyAsync( + b => b.Id == orderBranchId && b.CafeId == cafeId, + cancellationToken); + if (!branchOk) + orderBranchId = null; + } + + var orderGuestName = string.IsNullOrWhiteSpace(request.GuestName) + ? null + : request.GuestName.Trim(); + if (orderGuestName is null && !string.IsNullOrWhiteSpace(guestName)) + orderGuestName = guestName.Trim(); + + var orderGuestPhone = NormalizeGuestPhone(request.GuestPhone); + if (orderGuestPhone is null) + orderGuestPhone = NormalizeGuestPhone(guestPhone); + + var orderItems = new List(); + foreach (var line in request.Items) + { + var menuItem = menuItems.First(m => m.Id == line.MenuItemId); + var unitPrice = unitPriceByMenuItemId?.GetValueOrDefault(menuItem.Id) ?? menuItem.Price; + orderItems.Add(new OrderItem + { + MenuItemId = menuItem.Id, + Quantity = line.Quantity, + UnitPrice = unitPrice, + Notes = line.Notes + }); + } + + string? customerId = request.CustomerId; + if (string.IsNullOrEmpty(customerId) && !string.IsNullOrWhiteSpace(orderGuestPhone)) + customerId = await ResolveCustomerIdFromPhoneAsync( + cafeId, + orderGuestPhone, + orderGuestName ?? guestName, + cancellationToken); + + if (string.IsNullOrEmpty(orderBranchId) && !string.IsNullOrEmpty(tableId)) + { + var tableBranch = await _db.Tables + .Where(t => t.Id == tableId && t.CafeId == cafeId) + .Select(t => t.BranchId) + .FirstOrDefaultAsync(cancellationToken); + if (!string.IsNullOrEmpty(tableBranch)) + orderBranchId = tableBranch; + } + + var displayNumber = await AllocateDisplayNumberAsync(cafeId, cancellationToken); + var order = new Order + { + CafeId = cafeId, + BranchId = orderBranchId, + TableId = tableId, + ReservationId = request.ReservationId, + GuestName = orderGuestName, + GuestPhone = orderGuestPhone, + CustomerId = customerId, + EmployeeId = employeeId, + OrderType = request.OrderType, + Source = source, + Status = OrderStatus.Pending, + DisplayNumber = displayNumber, + StatusUpdatedAt = DateTime.UtcNow, + GuestTrackingToken = source == OrderSource.GuestQr ? OrderTrackingHelper.NewTrackingToken() : null, + CouponId = request.CouponId, + Items = orderItems + }; + + _db.Orders.Add(order); + await RecalculateOrderTotalsAsync(order, cafeId, cancellationToken); + await _db.SaveChangesAsync(cancellationToken); + + await _inventory.DeductForOrderAsync( + cafeId, + order.Id, + orderItems.Select(i => (i.MenuItemId, i.Quantity)).ToList(), + cancellationToken); + + var loaded = await LoadOrderAsync(cafeId, order.Id, cancellationToken); + if (loaded is not null) + { + await _kdsNotifier.NotifyOrderCreatedAsync(cafeId, MapLiveOrder(loaded), cancellationToken); + if (!string.IsNullOrEmpty(loaded.TableId)) + await _kdsNotifier.NotifyTableStatusChangedAsync(cafeId, cancellationToken); + if (source == OrderSource.GuestQr) + await _orderNotifications.NotifyGuestOrderPlacedAsync(loaded, MapLiveOrder(loaded), cancellationToken); + } + + return loaded is null + ? new OrderServiceResult(false, null, "INVALID_ORDER") + : new OrderServiceResult(true, MapOrder(loaded)); + } + + private static void ApplyGuestFields( + Order order, + CreateOrderRequest request, + string? guestPhone, + string? guestName) + { + if (!string.IsNullOrWhiteSpace(request.GuestName)) + order.GuestName = request.GuestName.Trim(); + else if (!string.IsNullOrWhiteSpace(guestName)) + order.GuestName = guestName.Trim(); + + var phone = NormalizeGuestPhone(request.GuestPhone) ?? NormalizeGuestPhone(guestPhone); + if (phone is not null) + order.GuestPhone = phone; + } + + private static string? NormalizeGuestPhone(string? phone) + { + if (string.IsNullOrWhiteSpace(phone)) return null; + var normalized = PhoneNormalizer.Normalize(phone); + return PhoneNormalizer.IsValidIranMobile(normalized) ? normalized : null; + } + + private async Task AppendLinesToOrderAsync( + Order order, + IReadOnlyList lines, + string cafeId, + IReadOnlyDictionary? unitPriceByMenuItemId, + CancellationToken cancellationToken) + { + var menuItemIds = lines.Select(i => i.MenuItemId).Distinct().ToList(); + var menuItems = await _db.MenuItems + .Where(m => m.CafeId == cafeId && menuItemIds.Contains(m.Id) && m.IsAvailable) + .ToListAsync(cancellationToken); + + if (menuItems.Count != menuItemIds.Count) + throw new InvalidOperationException("Invalid menu items."); + + foreach (var line in lines) + { + var menuItem = menuItems.First(m => m.Id == line.MenuItemId); + var unitPrice = unitPriceByMenuItemId?.GetValueOrDefault(menuItem.Id) ?? menuItem.Price; + order.Items.Add(new OrderItem + { + OrderId = order.Id, + MenuItemId = menuItem.Id, + Quantity = line.Quantity, + UnitPrice = unitPrice, + Notes = line.Notes + }); + } + } + + private async Task ResolveCustomerIdFromPhoneAsync( + string cafeId, + string phone, + string? guestName, + CancellationToken cancellationToken) + { + var existing = await _db.Customers.FirstOrDefaultAsync( + c => c.CafeId == cafeId && c.Phone == phone, + cancellationToken); + + if (existing is not null) + return existing.Id; + + if (string.IsNullOrWhiteSpace(guestName)) + return null; + + var customer = new Customer + { + CafeId = cafeId, + Name = guestName.Trim(), + Phone = phone, + Group = CustomerGroup.New + }; + _db.Customers.Add(customer); + await _db.SaveChangesAsync(cancellationToken); + return customer.Id; + } + + private async Task ResolveEffectiveTaxRateAsync( + string cafeId, + string? branchId, + CancellationToken cancellationToken) + { + var cafe = await _db.Cafes.AsNoTracking() + .FirstOrDefaultAsync(c => c.Id == cafeId, cancellationToken); + + if (cafe is not null && cafe.DefaultTaxRate > 0) + { + if (!string.IsNullOrEmpty(branchId) && cafe.AllowBranchTaxOverride) + { + var branchRate = await _db.Branches.AsNoTracking() + .Where(b => b.Id == branchId && b.CafeId == cafeId) + .Select(b => b.TaxRate) + .FirstOrDefaultAsync(cancellationToken); + if (branchRate is > 0) + return branchRate.Value; + } + + return cafe.DefaultTaxRate; + } + + var taxTableRate = await _db.Taxes + .Where(t => t.CafeId == cafeId && t.IsDefault) + .Select(t => t.Rate) + .FirstOrDefaultAsync(cancellationToken); + + return taxTableRate > 0 ? taxTableRate : 9m; + } + + private async Task RecalculateOrderTotalsAsync( + Order order, + string cafeId, + CancellationToken cancellationToken) + { + await _db.Entry(order).Collection(o => o.Items).LoadAsync(cancellationToken); + + var subtotal = order.Items.Where(i => !i.IsVoided).Sum(i => i.UnitPrice * i.Quantity); + var discountAmount = 0m; + + if (!string.IsNullOrEmpty(order.CouponId)) + { + var coupon = await _db.Coupons.FirstOrDefaultAsync( + c => c.Id == order.CouponId && c.CafeId == cafeId && c.IsActive, + cancellationToken); + + if (coupon is not null + && coupon.DeletedAt is null + && (coupon.UsageLimit is null || coupon.UsedCount < coupon.UsageLimit) + && (coupon.StartsAt is null || coupon.StartsAt <= DateTime.UtcNow) + && (coupon.ExpiresAt is null || coupon.ExpiresAt >= DateTime.UtcNow) + && (coupon.MinOrderAmount is null || subtotal >= coupon.MinOrderAmount)) + { + discountAmount = CouponService.CalculateDiscount(coupon, subtotal); + } + } + + var defaultTaxRate = await ResolveEffectiveTaxRateAsync(cafeId, order.BranchId, cancellationToken); + + var taxable = subtotal - discountAmount; + var taxTotal = Math.Round(taxable * defaultTaxRate / 100m, 0); + + order.Subtotal = subtotal; + order.DiscountAmount = discountAmount; + order.TaxTotal = taxTotal; + order.Total = taxable + taxTotal; + } + + private async Task FindOpenOrderForTableAsync( + string cafeId, + string tableId, + CancellationToken cancellationToken) => + await ApplyOrderIncludes(_db.Orders) + .Where(o => o.CafeId == cafeId + && o.TableId == tableId + && OpenForPaymentStatuses.Contains(o.Status)) + .OrderBy(o => o.CreatedAt) + .FirstOrDefaultAsync(cancellationToken); + + public async Task UpdateStatusAsync(string cafeId, string orderId, OrderStatus status, CancellationToken cancellationToken = default) + { + var order = await _db.Orders.FirstOrDefaultAsync(o => o.Id == orderId && o.CafeId == cafeId, cancellationToken); + if (order is null) return null; + + if (status == OrderStatus.Cancelled && !OpenForPaymentStatuses.Contains(order.Status)) + return null; + + order.Status = status; + order.StatusUpdatedAt = DateTime.UtcNow; + await _db.SaveChangesAsync(cancellationToken); + + await _kdsNotifier.NotifyOrderStatusChangedAsync(cafeId, orderId, status, cancellationToken); + if (!string.IsNullOrEmpty(order.TableId)) + await _kdsNotifier.NotifyTableStatusChangedAsync(cafeId, cancellationToken); + + await _deliverySync.SyncInternalStatusAsync(cafeId, orderId, status, cancellationToken); + + var loaded = await LoadOrderAsync(cafeId, orderId, cancellationToken); + if (loaded is not null) + await _orderNotifications.NotifyOrderStatusChangedAsync(loaded, cancellationToken); + + return await GetOrderAsync(cafeId, orderId, cancellationToken); + } + + public async Task>> RecordPaymentsAsync( + string cafeId, + string orderId, + RecordPaymentsRequest request, + string? userId, + CancellationToken cancellationToken = default) + { + var order = await _db.Orders + .Include(o => o.Payments) + .FirstOrDefaultAsync(o => o.Id == orderId && o.CafeId == cafeId, cancellationToken); + + if (order is null) + return new OrderServiceResult>(false, null, "ORDER_NOT_FOUND"); + + var branchId = await ResolveOrderBranchIdAsync(order, cafeId, cancellationToken); + if (string.IsNullOrEmpty(branchId)) + return new OrderServiceResult>(false, null, "NO_OPEN_SHIFT", "branchId"); + + if (string.IsNullOrEmpty(order.BranchId)) + { + order.BranchId = branchId; + } + + var shiftCheck = await _shiftService.RequireOpenShiftForBranchAsync( + cafeId, branchId, cancellationToken); + if (!shiftCheck.Success) + return new OrderServiceResult>(false, null, shiftCheck.ErrorCode, shiftCheck.Field); + + if (request.LoyaltyPointsToRedeem is > 0) + { + var redeem = await _loyalty.RedeemOnOrderAsync( + cafeId, order, request.LoyaltyPointsToRedeem.Value, cancellationToken); + if (!redeem.Success) + { + return new OrderServiceResult>( + false, null, redeem.ErrorCode ?? "LOYALTY_REDEEM_FAILED"); + } + + if (redeem.Data is { DiscountToman: > 0 }) + { + order.DiscountAmount += redeem.Data.DiscountToman; + var taxRate = await ResolveEffectiveTaxRateAsync(cafeId, order.BranchId, cancellationToken); + var taxable = Math.Max(0, order.Subtotal - order.DiscountAmount); + order.TaxTotal = Math.Round(taxable * taxRate / 100m, 0); + order.Total = taxable + order.TaxTotal; + } + } + + var openShift = shiftCheck.Data!; + var createdBy = userId ?? order.EmployeeId ?? openShift.OpenedByUserId; + + var payments = request.Payments.Select(p => new Payment + { + OrderId = orderId, + Method = p.Method, + Amount = p.Amount, + Reference = p.Reference, + Status = PaymentStatus.Completed + }).ToList(); + + _db.Payments.AddRange(payments); + + var paidTotal = order.Payments.Where(p => p.Status == PaymentStatus.Completed).Sum(p => p.Amount) + + payments.Sum(p => p.Amount); + + if (paidTotal >= order.Total) + { + order.Status = OrderStatus.Delivered; + + if (!string.IsNullOrEmpty(order.ReservationId)) + { + var reservation = await _db.TableReservations.FirstOrDefaultAsync( + r => r.Id == order.ReservationId && r.CafeId == cafeId, + cancellationToken); + if (reservation is not null) + reservation.Status = ReservationStatus.Completed; + } + } + + await _db.SaveChangesAsync(cancellationToken); + + foreach (var payment in payments) + { + await _shiftService.RecordTransactionAsync( + cafeId, + openShift.Id, + CashTransactionType.OrderPayment, + payment.Method, + payment.Amount, + createdBy, + orderId, + null, + cancellationToken); + } + + if (!string.IsNullOrEmpty(order.TableId)) + await _kdsNotifier.NotifyTableStatusChangedAsync(cafeId, cancellationToken); + + if (paidTotal >= order.Total) + { + PrinterBackgroundJobs.QueueReceiptPrint(_scopeFactory, cafeId, orderId); + await _loyalty.ApplyEarnOnOrderPaidAsync(cafeId, order.CustomerId, paidTotal, cancellationToken); + await _deliverySync.SyncInternalStatusAsync(cafeId, orderId, OrderStatus.Delivered, cancellationToken); + } + + var dtos = payments.Select(p => new PaymentDto(p.Id, p.Method, p.Amount, p.Status, p.Reference)).ToList(); + return new OrderServiceResult>(true, dtos); + } + + private static IQueryable ApplyOrderIncludes(IQueryable query) => + query + .Include(o => o.Items) + .ThenInclude(i => i.MenuItem) + .Include(o => o.Table) + .Include(o => o.Customer) + .Include(o => o.Reservation) + .Include(o => o.Payments); + + private async Task LoadOrderAsync(string cafeId, string orderId, CancellationToken cancellationToken) => + await ApplyOrderIncludes(_db.Orders) + .FirstOrDefaultAsync(o => o.Id == orderId && o.CafeId == cafeId, cancellationToken); + + private async Task ResolveOrderBranchIdAsync( + Order order, + string cafeId, + CancellationToken cancellationToken) + { + if (!string.IsNullOrEmpty(order.BranchId)) + return order.BranchId; + + if (!string.IsNullOrEmpty(order.TableId)) + { + var tableBranchId = await _db.Tables + .Where(t => t.Id == order.TableId && t.CafeId == cafeId) + .Select(t => t.BranchId) + .FirstOrDefaultAsync(cancellationToken); + if (!string.IsNullOrEmpty(tableBranchId)) + return tableBranchId; + } + + return await _db.Branches + .Where(b => b.CafeId == cafeId && b.IsActive) + .OrderBy(b => b.CreatedAt) + .Select(b => b.Id) + .FirstOrDefaultAsync(cancellationToken); + } + + private async Task AllocateDisplayNumberAsync(string cafeId, CancellationToken cancellationToken) + { + var max = await _db.Orders + .Where(o => o.CafeId == cafeId) + .MaxAsync(o => (int?)o.DisplayNumber, cancellationToken); + return (max ?? 0) + 1; + } + + private static OrderDto MapOrder(Order o) + { + var paid = o.Payments + .Where(p => p.Status == PaymentStatus.Completed) + .Sum(p => p.Amount); + + return new OrderDto( + o.Id, + o.CafeId, + o.BranchId, + o.TableId, + o.Table?.Number, + o.GuestName, + o.GuestPhone, + o.Customer?.Name, + o.Customer?.Phone, + o.CustomerId, + o.EmployeeId, + o.OrderType, + o.Source, + o.Status, + o.Subtotal, + o.TaxTotal, + o.DiscountAmount, + o.Total, + paid, + o.CreatedAt, + o.DisplayNumber > 0 ? o.DisplayNumber : ReceiptPrintFormatting.StableDisplayNumberFromId(o.Id), + o.Items.Select(i => new OrderItemDto( + i.Id, + i.MenuItemId, + i.MenuItem?.Name ?? "", + i.Quantity, + i.UnitPrice, + i.Notes, + i.IsVoided, + i.VoidedAt)).ToList(), + o.Payments.Select(p => new PaymentDto(p.Id, p.Method, p.Amount, p.Status, p.Reference)).ToList()); + } + + private static LiveOrderDto MapLiveOrder(Order o) => new( + o.Id, + o.DisplayNumber > 0 ? o.DisplayNumber : ReceiptPrintFormatting.StableDisplayNumberFromId(o.Id), + o.Status, + o.Table?.Number, + o.OrderType, + o.Total, + o.CreatedAt, + o.Items.Select(i => new OrderItemDto( + i.Id, + i.MenuItemId, + i.MenuItem?.Name ?? "", + i.Quantity, + i.UnitPrice, + i.Notes, + i.IsVoided, + i.VoidedAt)).ToList()); +} diff --git a/src/Meezi.API/Services/OrderTrackingHelper.cs b/src/Meezi.API/Services/OrderTrackingHelper.cs new file mode 100644 index 0000000..06baec0 --- /dev/null +++ b/src/Meezi.API/Services/OrderTrackingHelper.cs @@ -0,0 +1,90 @@ +using Meezi.API.Models.Orders; +using Meezi.API.Models.Public; +using Meezi.Core.Entities; +using Meezi.Core.Enums; +using Meezi.API.Services.Printing; + +namespace Meezi.API.Services; + +public static class OrderTrackingHelper +{ + private static readonly (string Key, int Rank)[] Flow = + [ + ("submitted", 0), + ("seen", 1), + ("preparing", 2), + ("ready", 3), + ("done", 4) + ]; + + public static string NewTrackingToken() => Guid.NewGuid().ToString("N"); + + public static OrderTrackDto BuildTrackDto(Order order) + { + var steps = BuildSteps(order.Status); + return new OrderTrackDto( + order.Id, + ReceiptPrintFormatting.OrderNumberLabel(order.DisplayNumber > 0 ? order.DisplayNumber : ReceiptPrintFormatting.StableDisplayNumberFromId(order.Id)), + order.Status, + StatusLabel(order.Status), + order.Total, + order.Table?.Number, + order.CreatedAt, + order.StatusUpdatedAt, + order.GuestTrackingToken ?? string.Empty, + steps, + order.Items + .Where(i => !i.IsVoided) + .Select(i => new OrderItemDto( + i.Id, + i.MenuItemId, + i.MenuItem?.Name ?? "", + i.Quantity, + i.UnitPrice, + i.Notes, + i.IsVoided, + i.VoidedAt)) + .ToList()); + } + + public static IReadOnlyList BuildSteps(OrderStatus status) + { + if (status == OrderStatus.Cancelled) + { + return + [ + new OrderTrackingStepDto("submitted", "submitted", true, false), + new OrderTrackingStepDto("cancelled", "cancelled", true, true) + ]; + } + + var rank = StatusRank(status); + return Flow.Select(step => + { + var isComplete = rank > step.Rank || status == OrderStatus.Delivered; + var isCurrent = rank == step.Rank && status is not OrderStatus.Delivered; + return new OrderTrackingStepDto(step.Key, step.Key, isComplete, isCurrent); + }).ToList(); + } + + private static int StatusRank(OrderStatus status) => status switch + { + OrderStatus.Pending => 0, + OrderStatus.Confirmed => 1, + OrderStatus.Preparing => 2, + OrderStatus.Ready => 3, + OrderStatus.Delivered => 4, + _ => 0 + }; + + public static string StatusLabel(OrderStatus status) => status switch + { + OrderStatus.Pending => "pending", + OrderStatus.Confirmed => "seen", + OrderStatus.Preparing => "preparing", + OrderStatus.Ready => "ready", + OrderStatus.Delivered => "done", + OrderStatus.Cancelled => "cancelled", + _ => "pending" + }; +} diff --git a/src/Meezi.API/Services/PlanLimitChecker.cs b/src/Meezi.API/Services/PlanLimitChecker.cs new file mode 100644 index 0000000..041506e --- /dev/null +++ b/src/Meezi.API/Services/PlanLimitChecker.cs @@ -0,0 +1,125 @@ +using Meezi.Infrastructure.Services.Platform; +using Meezi.Core.Enums; +using Meezi.Core.Interfaces; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using StackExchange.Redis; + +namespace Meezi.API.Services; + +public interface IPlanLimitChecker +{ + Task<(bool Allowed, string? ErrorCode, string? Message)> CheckAsync( + HttpContext context, + ITenantContext tenant, + CancellationToken cancellationToken = default); +} + +public class PlanLimitChecker : IPlanLimitChecker +{ + private readonly AppDbContext _db; + private readonly IConnectionMultiplexer _redis; + private readonly IPlatformCatalogService _platformCatalog; + + public PlanLimitChecker( + AppDbContext db, + IConnectionMultiplexer redis, + IPlatformCatalogService platformCatalog) + { + _db = db; + _redis = redis; + _platformCatalog = platformCatalog; + } + + public async Task<(bool Allowed, string? ErrorCode, string? Message)> CheckAsync( + HttpContext context, + ITenantContext tenant, + CancellationToken cancellationToken = default) + { + if (tenant.IsSystemAdmin || !tenant.IsAuthenticated || tenant.PlanTier is null || string.IsNullOrEmpty(tenant.CafeId)) + return (true, null, null); + + var method = context.Request.Method; + var path = context.Request.Path.Value ?? string.Empty; + + if (method != HttpMethods.Post) + return (true, null, null); + + var cafeId = tenant.CafeId; + var tier = tenant.PlanTier.Value; + + var ordersPath = $"/api/cafes/{cafeId}/orders"; + if (method == HttpMethods.Post && + path.StartsWith(ordersPath, StringComparison.OrdinalIgnoreCase) && + (path.Equals(ordersPath, StringComparison.OrdinalIgnoreCase) || + path.Equals($"{ordersPath}/", StringComparison.OrdinalIgnoreCase))) + { + var limits = await _platformCatalog.GetLimitsAsync(tier, cancellationToken); + var maxOrders = limits.MaxOrdersPerDay; + if (maxOrders == int.MaxValue) + return (true, null, null); + + var todayStart = DateTime.UtcNow.Date; + var count = await _db.Orders + .CountAsync(o => o.CafeId == cafeId && o.CreatedAt >= todayStart, cancellationToken); + + if (count >= maxOrders) + return (false, "PLAN_LIMIT_REACHED", "Daily order limit reached for your plan. Please upgrade."); + } + + var customersPath = $"/api/cafes/{cafeId}/customers"; + if (path.StartsWith(customersPath, StringComparison.OrdinalIgnoreCase) && + (path.Equals(customersPath, StringComparison.OrdinalIgnoreCase) || + path.Equals($"{customersPath}/", StringComparison.OrdinalIgnoreCase))) + { + var limitsCustomers = await _platformCatalog.GetLimitsAsync(tier, cancellationToken); + var maxCustomers = limitsCustomers.MaxCustomers; + if (maxCustomers == int.MaxValue) + return (true, null, null); + + var count = await _db.Customers + .CountAsync(c => c.CafeId == cafeId, cancellationToken); + + if (count >= maxCustomers) + return (false, "PLAN_LIMIT_REACHED", "Customer limit reached for your plan. Please upgrade."); + } + + var branchesPath = $"/api/cafes/{cafeId}/branches"; + if (path.StartsWith(branchesPath, StringComparison.OrdinalIgnoreCase) && + (path.Equals(branchesPath, StringComparison.OrdinalIgnoreCase) || + path.Equals($"{branchesPath}/", StringComparison.OrdinalIgnoreCase))) + { + var limitsBranches = await _platformCatalog.GetLimitsAsync(tier, cancellationToken); + var maxBranches = limitsBranches.MaxBranches; + if (maxBranches == int.MaxValue) + return (true, null, null); + + var branchCount = await _db.Branches.CountAsync(b => b.CafeId == cafeId, cancellationToken); + if (branchCount >= maxBranches) + return (false, "PLAN_LIMIT_REACHED", "Branch limit reached for your plan. Please upgrade."); + } + + var smsCampaignPath = $"/api/cafes/{cafeId}/sms/campaign"; + if (path.Equals(smsCampaignPath, StringComparison.OrdinalIgnoreCase) || + path.Equals($"{smsCampaignPath}/", StringComparison.OrdinalIgnoreCase)) + { + var limitsSms = await _platformCatalog.GetLimitsAsync(tier, cancellationToken); + var maxSms = limitsSms.MaxSmsPerMonth; + if (maxSms == 0) + return (false, "PLAN_LIMIT_REACHED", "SMS is not available on the Free plan. Please upgrade."); + + if (maxSms == int.MaxValue) + return (true, null, null); + + var monthKey = $"sms:usage:{cafeId}:{DateTime.UtcNow:yyyy-MM}"; + var redis = _redis.GetDatabase(); + var used = await redis.StringGetAsync(monthKey); + var usedCount = used.HasValue ? (int)used : 0; + + if (usedCount >= maxSms) + return (false, "PLAN_LIMIT_REACHED", "Monthly SMS limit reached for your plan. Please upgrade."); + } + + return (true, null, null); + } +} diff --git a/src/Meezi.API/Services/PosDeviceService.cs b/src/Meezi.API/Services/PosDeviceService.cs new file mode 100644 index 0000000..f62816f --- /dev/null +++ b/src/Meezi.API/Services/PosDeviceService.cs @@ -0,0 +1,120 @@ +using System.Net.Http.Json; +using System.Text.Json; +using Meezi.API.Models.Printing; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; + +namespace Meezi.API.Services; + +public record PosDeviceResult(bool Success, bool Skipped, string? ErrorCode, string? Detail = null) +{ + public static PosDeviceResult Ok() => new(true, false, null); + public static PosDeviceResult SkippedNotConfigured() => new(true, true, null); + public static PosDeviceResult Fail(string code, string? detail = null) => new(false, false, code, detail); +} + +public interface IPosDeviceService +{ + Task SendPaymentRequestAsync( + string cafeId, + string branchId, + PosPaymentRequest request, + CancellationToken ct = default); +} + +public class PosDeviceService : IPosDeviceService +{ + private const int DefaultPort = 8088; + private static readonly TimeSpan RequestTimeout = TimeSpan.FromSeconds(90); + + private readonly AppDbContext _db; + private readonly IHttpClientFactory _httpClientFactory; + private readonly ILogger _logger; + + public PosDeviceService( + AppDbContext db, + IHttpClientFactory httpClientFactory, + ILogger logger) + { + _db = db; + _httpClientFactory = httpClientFactory; + _logger = logger; + } + + public async Task SendPaymentRequestAsync( + string cafeId, + string branchId, + PosPaymentRequest request, + CancellationToken ct = default) + { + if (request.Amount <= 0) + return PosDeviceResult.Fail("INVALID_AMOUNT"); + + var branch = await _db.Branches + .AsNoTracking() + .FirstOrDefaultAsync(b => b.Id == branchId && b.CafeId == cafeId, ct); + + if (branch is null) + return PosDeviceResult.Fail("BRANCH_NOT_FOUND"); + + if (string.IsNullOrWhiteSpace(branch.PosDeviceIp)) + return PosDeviceResult.SkippedNotConfigured(); + + var port = branch.PosDevicePort is > 0 and <= 65535 + ? branch.PosDevicePort.Value + : DefaultPort; + + var order = await _db.Orders + .AsNoTracking() + .FirstOrDefaultAsync(o => o.Id == request.OrderId && o.CafeId == cafeId, ct); + + if (order is null) + return PosDeviceResult.Fail("ORDER_NOT_FOUND"); + + var payload = new + { + amount = (long)Math.Round(request.Amount, 0, MidpointRounding.AwayFromZero), + orderId = request.OrderId, + branchId, + }; + + var url = $"http://{branch.PosDeviceIp!.Trim()}:{port}/pay"; + + try + { + var client = _httpClientFactory.CreateClient(nameof(PosDeviceService)); + client.Timeout = RequestTimeout; + + using var response = await client.PostAsJsonAsync(url, payload, ct); + + if (!response.IsSuccessStatusCode) + { + var body = await response.Content.ReadAsStringAsync(ct); + _logger.LogWarning( + "POS device returned {Status} for {Url}: {Body}", + (int)response.StatusCode, + url, + body.Length > 200 ? body[..200] : body); + return PosDeviceResult.Fail( + "POS_DEVICE_REJECTED", + $"HTTP {(int)response.StatusCode}"); + } + + return PosDeviceResult.Ok(); + } + catch (TaskCanceledException) when (!ct.IsCancellationRequested) + { + return PosDeviceResult.Fail("POS_DEVICE_TIMEOUT"); + } + catch (HttpRequestException ex) + { + _logger.LogWarning(ex, "POS device connection failed for {Url}", url); + return PosDeviceResult.Fail("POS_DEVICE_CONNECTION_FAILED", ex.Message); + } + catch (JsonException ex) + { + _logger.LogWarning(ex, "POS device response invalid for {Url}", url); + return PosDeviceResult.Fail("POS_DEVICE_CONNECTION_FAILED", ex.Message); + } + } +} diff --git a/src/Meezi.API/Services/Printing/EscPosBuilder.cs b/src/Meezi.API/Services/Printing/EscPosBuilder.cs new file mode 100644 index 0000000..aa00a58 --- /dev/null +++ b/src/Meezi.API/Services/Printing/EscPosBuilder.cs @@ -0,0 +1,92 @@ +using System.Text; + +namespace Meezi.API.Services.Printing; + +public class EscPosBuilder +{ + private readonly List _buffer = []; + + public EscPosBuilder Initialize() + { + _buffer.AddRange([0x1B, 0x40]); + return this; + } + + public EscPosBuilder SetEncoding() + { + _buffer.AddRange([0x1B, 0x74, 0x25]); + return this; + } + + public EscPosBuilder AlignCenter() + { + _buffer.AddRange([0x1B, 0x61, 0x01]); + return this; + } + + public EscPosBuilder AlignRight() + { + _buffer.AddRange([0x1B, 0x61, 0x02]); + return this; + } + + public EscPosBuilder AlignLeft() + { + _buffer.AddRange([0x1B, 0x61, 0x00]); + return this; + } + + public EscPosBuilder Bold(bool on) + { + _buffer.AddRange([0x1B, 0x45, on ? (byte)1 : (byte)0]); + return this; + } + + public EscPosBuilder DoubleHeight(bool on) + { + _buffer.AddRange([0x1B, 0x21, on ? (byte)0x10 : (byte)0x00]); + return this; + } + + public EscPosBuilder Text(string text) + { + _buffer.AddRange(Encoding.UTF8.GetBytes(text)); + return this; + } + + public EscPosBuilder Line(string text = "") + { + return Text(text + "\n"); + } + + public EscPosBuilder Separator(int width = 48, char ch = '-') + { + return Line(new string(ch, Math.Min(width, 64))); + } + + public EscPosBuilder TwoColumns(string left, string right, int totalWidth = 48) + { + var safeLeft = left.Length > totalWidth ? left[..totalWidth] : left; + var safeRight = right.Length > totalWidth ? right[^totalWidth..] : right; + var pad = Math.Max(1, totalWidth - safeLeft.Length - safeRight.Length); + var line = safeLeft + new string(' ', pad) + safeRight; + if (line.Length > totalWidth) + line = line[..totalWidth]; + return Line(line); + } + + public EscPosBuilder Feed(int lines = 3) + { + for (var i = 0; i < lines; i++) + _buffer.Add(0x0A); + return this; + } + + public EscPosBuilder Cut() + { + _buffer.AddRange([0x1D, 0x56, 0x42, 0x03]); + return this; + } + + public byte[] Build() => [.. _buffer]; +} diff --git a/src/Meezi.API/Services/Printing/NetworkPrinterService.cs b/src/Meezi.API/Services/Printing/NetworkPrinterService.cs new file mode 100644 index 0000000..a708327 --- /dev/null +++ b/src/Meezi.API/Services/Printing/NetworkPrinterService.cs @@ -0,0 +1,256 @@ +using System.Net.Sockets; +using Meezi.API.Models.Orders; +using Meezi.API.Models.Printing; +using Meezi.Core.Entities; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; + +namespace Meezi.API.Services.Printing; + +public record PrintResult(bool Success, string? ErrorCode, string? ErrorDetail = null) +{ + public static PrintResult Ok() => new(true, null); + public static PrintResult Fail(string code, string? detail = null) => new(false, code, detail); +} + +public interface IPrinterService +{ + Task PrintReceiptAsync(string cafeId, string orderId, CancellationToken ct = default); + Task PrintKitchenTicketAsync( + string cafeId, + string orderId, + CancellationToken ct = default); + Task TestPrintAsync(string printerIp, int port, CancellationToken ct = default); +} + +public class NetworkPrinterService : IPrinterService +{ + private readonly AppDbContext _db; + private readonly IOrderService _orders; + private readonly ReceiptBuilder _receiptBuilder; + private readonly ILogger _logger; + + public NetworkPrinterService( + AppDbContext db, + IOrderService orders, + ReceiptBuilder receiptBuilder, + ILogger logger) + { + _db = db; + _orders = orders; + _receiptBuilder = receiptBuilder; + _logger = logger; + } + + public async Task PrintReceiptAsync(string cafeId, string orderId, CancellationToken ct = default) + { + var ctx = await BuildContextAsync(cafeId, orderId, ct); + if (ctx is null) + return PrintResult.Fail("ORDER_NOT_FOUND"); + + if (string.IsNullOrWhiteSpace(ctx.Value.branch.ReceiptPrinterIp)) + return PrintResult.Fail("PRINTER_NOT_CONFIGURED"); + + var bytes = _receiptBuilder.BuildReceipt(ctx.Value.printCtx); + return await SendToPrinterAsync( + ctx.Value.branch.ReceiptPrinterIp!, + ctx.Value.branch.ReceiptPrinterPort ?? 9100, + bytes, + ct); + } + + public async Task PrintKitchenTicketAsync( + string cafeId, + string orderId, + CancellationToken ct = default) + { + var ctx = await BuildContextAsync(cafeId, orderId, ct); + if (ctx is null) + return PrintResult.Fail("ORDER_NOT_FOUND"); + + var order = ctx.Value.printCtx.Order; + var activeItems = order.Items.Where(i => !i.IsVoided).ToList(); + if (activeItems.Count == 0) + return PrintResult.Ok(); + + var menuItemIds = activeItems.Select(i => i.MenuItemId).Distinct().ToList(); + var categoryStations = await ( + from m in _db.MenuItems.AsNoTracking() + join c in _db.MenuCategories.AsNoTracking() on m.CategoryId equals c.Id + where menuItemIds.Contains(m.Id) && m.CafeId == cafeId + select new { m.Id, c.KitchenStationId } + ).ToListAsync(ct); + + var stationIds = categoryStations + .Select(x => x.KitchenStationId) + .Where(id => !string.IsNullOrEmpty(id)) + .Distinct() + .ToList(); + + var stations = stationIds.Count == 0 + ? [] + : await _db.KitchenStations + .AsNoTracking() + .Where(s => stationIds.Contains(s.Id) && s.CafeId == cafeId) + .ToListAsync(ct); + + var groups = activeItems + .GroupBy(item => + { + var cat = categoryStations.FirstOrDefault(c => c.Id == item.MenuItemId); + return cat?.KitchenStationId; + }) + .ToList(); + + PrintResult? lastFail = null; + var anyPrinted = false; + + foreach (var group in groups) + { + var station = string.IsNullOrEmpty(group.Key) + ? null + : stations.FirstOrDefault(s => s.Id == group.Key); + + string? ip; + int port; + string? stationLabel = null; + + if (station is not null && !string.IsNullOrWhiteSpace(station.PrinterIp)) + { + ip = station.PrinterIp; + port = station.PrinterPort; + stationLabel = station.Name; + } + else if (!string.IsNullOrWhiteSpace(ctx.Value.branch.KitchenPrinterIp)) + { + ip = ctx.Value.branch.KitchenPrinterIp; + port = ctx.Value.branch.KitchenPrinterPort ?? 9100; + } + else + { + lastFail = PrintResult.Fail("KITCHEN_PRINTER_NOT_CONFIGURED"); + continue; + } + + var itemsOnly = group.ToList(); + var bytes = _receiptBuilder.BuildKitchenTicket( + ctx.Value.printCtx with { StationName = stationLabel }, + itemsOnly); + var result = await SendToPrinterAsync(ip!, port, bytes, ct); + if (result.Success) + anyPrinted = true; + else + lastFail = result; + } + + return anyPrinted ? PrintResult.Ok() : lastFail ?? PrintResult.Fail("KITCHEN_PRINTER_NOT_CONFIGURED"); + } + + public async Task TestPrintAsync(string printerIp, int port, CancellationToken ct = default) + { + if (string.IsNullOrWhiteSpace(printerIp)) + return PrintResult.Fail("PRINTER_NOT_CONFIGURED"); + + var bytes = _receiptBuilder.BuildTestPage(); + return await SendToPrinterAsync(printerIp.Trim(), port, bytes, ct); + } + + private async Task<(Branch branch, ReceiptPrintContext printCtx)?> BuildContextAsync( + string cafeId, + string orderId, + CancellationToken ct) + { + var order = await _orders.GetOrderAsync(cafeId, orderId, ct); + if (order is null || string.IsNullOrEmpty(order.BranchId)) + return null; + + var branch = await _db.Branches + .AsNoTracking() + .Include(b => b.Cafe) + .FirstOrDefaultAsync(b => b.Id == order.BranchId && b.CafeId == cafeId, ct); + + if (branch is null) + return null; + + var print = new ReceiptPrintContext( + order, + branch.Cafe.Name, + branch.Name, + branch.ReceiptHeader, + branch.ReceiptFooter, + branch.WifiPassword, + branch.PaperWidthMm is 58 or 80 ? branch.PaperWidthMm : 80, + branch.AutoCutEnabled); + + return (branch, printCtx: print); + } + + private async Task SendToPrinterAsync( + string ip, + int port, + byte[] data, + CancellationToken ct) + { + try + { + using var client = new TcpClient(); + using var timeout = CancellationTokenSource.CreateLinkedTokenSource(ct); + timeout.CancelAfter(TimeSpan.FromSeconds(5)); + await client.ConnectAsync(ip, port, timeout.Token); + await using var stream = client.GetStream(); + await stream.WriteAsync(data, timeout.Token); + await stream.FlushAsync(timeout.Token); + + _logger.LogInformation("Printed {Bytes} bytes to {Ip}:{Port}", data.Length, ip, port); + return PrintResult.Ok(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Print failed to {Ip}:{Port}", ip, port); + return PrintResult.Fail("PRINTER_CONNECTION_FAILED", ex.Message); + } + } +} + +public static class PrinterBackgroundJobs +{ + public static void QueueReceiptPrint(IServiceScopeFactory scopeFactory, string cafeId, string orderId) + { + _ = Task.Run(async () => + { + await using var scope = scopeFactory.CreateAsyncScope(); + var logger = scope.ServiceProvider.GetRequiredService>(); + try + { + var printer = scope.ServiceProvider.GetRequiredService(); + var result = await printer.PrintReceiptAsync(cafeId, orderId, CancellationToken.None); + if (!result.Success) + logger.LogWarning("Auto-print receipt failed for {OrderId}: {Code}", orderId, result.ErrorCode); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Auto-print receipt failed for order {OrderId}", orderId); + } + }); + } + + public static void QueueKitchenPrint(IServiceScopeFactory scopeFactory, string cafeId, string orderId) + { + _ = Task.Run(async () => + { + await using var scope = scopeFactory.CreateAsyncScope(); + var logger = scope.ServiceProvider.GetRequiredService>(); + try + { + var printer = scope.ServiceProvider.GetRequiredService(); + var result = await printer.PrintKitchenTicketAsync(cafeId, orderId, CancellationToken.None); + if (!result.Success) + logger.LogWarning("Kitchen print failed for {OrderId}: {Code}", orderId, result.ErrorCode); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Kitchen print failed for order {OrderId}", orderId); + } + }); + } +} diff --git a/src/Meezi.API/Services/Printing/ReceiptBuilder.cs b/src/Meezi.API/Services/Printing/ReceiptBuilder.cs new file mode 100644 index 0000000..d7d88ae --- /dev/null +++ b/src/Meezi.API/Services/Printing/ReceiptBuilder.cs @@ -0,0 +1,132 @@ +using Meezi.API.Models.Orders; + +namespace Meezi.API.Services.Printing; + +public class ReceiptBuilder +{ + public byte[] BuildReceipt(ReceiptPrintContext ctx) + { + var order = ctx.Order; + var width = ReceiptPrintFormatting.LineWidth(ctx.PaperWidthMm); + var b = new EscPosBuilder(); + + b.Initialize().SetEncoding(); + + b.AlignCenter() + .Bold(true) + .DoubleHeight(true) + .Line(ctx.CafeName) + .DoubleHeight(false) + .Bold(false) + .Line(ctx.BranchName); + + if (!string.IsNullOrWhiteSpace(ctx.ReceiptHeader)) + b.Line(ctx.ReceiptHeader.Trim()); + + var orderNo = ReceiptPrintFormatting.OrderNumberLabel(order.DisplayNumber); + b.AlignRight() + .Line($"شماره سفارش: {orderNo}") + .Line($"تاریخ: {ReceiptPrintFormatting.ToShamsi(order.CreatedAt)}") + .Line($"میز: {order.TableNumber ?? "—"}"); + + if (!string.IsNullOrWhiteSpace(order.GuestName)) + b.Line($"مهمان: {order.GuestName}"); + else if (!string.IsNullOrWhiteSpace(order.CustomerName)) + b.Line($"مشتری: {order.CustomerName}"); + + b.Separator(width).AlignRight(); + + foreach (var item in order.Items.Where(i => !i.IsVoided)) + { + var itemTotal = ReceiptPrintFormatting.FormatCurrency(item.UnitPrice * item.Quantity); + var itemLine = $"{item.MenuItemName} × {item.Quantity}"; + b.TwoColumns(itemLine, itemTotal, width); + } + + b.Separator(width); + + if (order.DiscountAmount > 0) + b.TwoColumns("تخفیف", ReceiptPrintFormatting.FormatCurrency(order.DiscountAmount), width); + + if (order.TaxTotal > 0) + b.TwoColumns("مالیات", ReceiptPrintFormatting.FormatCurrency(order.TaxTotal), width); + + b.Bold(true) + .TwoColumns("مجموع کل", ReceiptPrintFormatting.FormatCurrency(order.Total), width) + .Bold(false); + + foreach (var payment in order.Payments) + { + var label = ReceiptPrintFormatting.PaymentMethodLabel(payment.Method); + b.TwoColumns(label, ReceiptPrintFormatting.FormatCurrency(payment.Amount), width); + } + + b.Separator(width).AlignCenter(); + + if (!string.IsNullOrWhiteSpace(ctx.WifiPassword)) + b.Line($"WiFi: {ctx.WifiPassword.Trim()}"); + + if (!string.IsNullOrWhiteSpace(ctx.ReceiptFooter)) + b.Line(ctx.ReceiptFooter.Trim()); + + b.Line("ممنون از انتخاب شما"); + + b.Feed(3); + if (ctx.AutoCutEnabled) + b.Cut(); + + return b.Build(); + } + + public byte[] BuildKitchenTicket(ReceiptPrintContext ctx, IReadOnlyList? itemsOnly = null) + { + var order = ctx.Order; + var items = itemsOnly ?? order.Items.Where(i => !i.IsVoided).ToList(); + var width = ReceiptPrintFormatting.LineWidth(ctx.PaperWidthMm); + var b = new EscPosBuilder(); + + b.Initialize() + .SetEncoding() + .AlignCenter() + .Bold(true) + .DoubleHeight(true) + .Line(string.IsNullOrWhiteSpace(ctx.StationName) ? "آشپزخانه" : ctx.StationName!) + .DoubleHeight(false) + .Bold(false) + .AlignRight() + .Line($"میز: {order.TableNumber ?? "—"} | #{ReceiptPrintFormatting.OrderNumberLabel(order.DisplayNumber)}") + .Line($"{DateTime.Now:HH:mm}") + .Separator(width); + + foreach (var item in items) + { + b.Bold(true) + .Line($"× {item.Quantity} {item.MenuItemName}") + .Bold(false); + + if (!string.IsNullOrWhiteSpace(item.Notes)) + b.Line($" ← {item.Notes}"); + } + + b.Feed(4); + if (ctx.AutoCutEnabled) + b.Cut(); + + return b.Build(); + } + + public byte[] BuildTestPage() + { + var b = new EscPosBuilder(); + b.Initialize() + .SetEncoding() + .AlignCenter() + .Bold(true) + .Line("Meezi Test Print ✓") + .Bold(false) + .Line(ReceiptPrintFormatting.ToShamsi(DateTime.Now)) + .Feed(3) + .Cut(); + return b.Build(); + } +} diff --git a/src/Meezi.API/Services/Printing/ReceiptPrintContext.cs b/src/Meezi.API/Services/Printing/ReceiptPrintContext.cs new file mode 100644 index 0000000..8b0c826 --- /dev/null +++ b/src/Meezi.API/Services/Printing/ReceiptPrintContext.cs @@ -0,0 +1,61 @@ +using Meezi.API.Models.Orders; +using Meezi.Core.Enums; + +namespace Meezi.API.Services.Printing; + +public record ReceiptPrintContext( + OrderDto Order, + string CafeName, + string BranchName, + string? ReceiptHeader, + string? ReceiptFooter, + string? WifiPassword, + int PaperWidthMm, + bool AutoCutEnabled, + string? StationName = null); + +public static class ReceiptPrintFormatting +{ + public static int LineWidth(int paperWidthMm) => paperWidthMm == 58 ? 32 : 48; + + public static string FormatCurrency(decimal amount) => + $"{amount:N0} ت"; + + public static string ToShamsi(DateTime dt) + { + var pc = new System.Globalization.PersianCalendar(); + return $"{pc.GetYear(dt)}/{pc.GetMonth(dt):D2}/{pc.GetDayOfMonth(dt):D2} {dt:HH:mm}"; + } + + public static string OrderNumberLabel(int displayNumber) => + displayNumber > 0 ? displayNumber.ToString() : "0"; + + public static string OrderNumberLabel(string orderId) => + OrderNumberLabel(StableDisplayNumberFromId(orderId)); + + public static int StableDisplayNumberFromId(string orderId) + { + if (string.IsNullOrWhiteSpace(orderId)) return 0; + var hex = orderId.Replace("-", "", StringComparison.Ordinal); + if (hex.Length == 32 + && ulong.TryParse(hex.AsSpan(0, 16), System.Globalization.NumberStyles.HexNumber, null, out var hi) + && ulong.TryParse(hex.AsSpan(16, 16), System.Globalization.NumberStyles.HexNumber, null, out var lo)) + { + return (int)((hi ^ lo) % 9_999_999) + 1; + } + + var digits = new string(orderId.Where(char.IsDigit).ToArray()); + if (digits.Length > 0 && int.TryParse(digits.Length > 9 ? digits[^9..] : digits, out var parsed)) + return parsed; + + return (int)((uint)Math.Abs(StringComparer.Ordinal.GetHashCode(hex)) % 999_999) + 1; + } + + public static string PaymentMethodLabel(PaymentMethod method) => method switch + { + PaymentMethod.Cash => "نقد", + PaymentMethod.Card => "کارت", + PaymentMethod.Credit => "اعتبار", + _ => method.ToString() + }; +} diff --git a/src/Meezi.API/Services/PublicService.cs b/src/Meezi.API/Services/PublicService.cs new file mode 100644 index 0000000..fd18a9e --- /dev/null +++ b/src/Meezi.API/Services/PublicService.cs @@ -0,0 +1,424 @@ +using System.Text.Json; +using Meezi.API.Models.Menu; +using Meezi.API.Models.Orders; +using Meezi.API.Models.Public; +using Meezi.Core.Constants; +using Meezi.Core.Discover; +using Meezi.Core.Entities; +using Meezi.Core.Enums; +using Meezi.Infrastructure.Data; +using Meezi.API.Security; +using Meezi.Infrastructure.Discover; +using Microsoft.EntityFrameworkCore; + +namespace Meezi.API.Services; + +public interface IPublicService +{ + Task> DiscoverAsync( + DiscoverFilterParams filters, + CancellationToken cancellationToken = default); + Task GetCafeAsync(string slug, CancellationToken cancellationToken = default); + Task GetMenuAsync(string slug, CancellationToken cancellationToken = default); + Task<(GuestOrderPlacedDto? Data, string? ErrorCode, string? ErrorMessage)> PlaceOrderAsync( + string slug, + GuestCreateOrderRequest request, + CancellationToken cancellationToken = default); + Task TrackOrderAsync( + string orderId, + string trackingToken, + CancellationToken cancellationToken = default); + Task<(ReservationDto? Data, string? ErrorCode, string? Message)> CreateReservationAsync( + string slug, + CreateReservationRequest request, + CancellationToken cancellationToken = default); + Task GetBranchMenuAsync( + string cafeId, + string branchId, + CancellationToken cancellationToken = default); + Task<(GuestQrOrderPlacedDto? Data, string? ErrorCode, string? Message)> PlaceBranchGuestOrderAsync( + string cafeId, + string branchId, + PlaceGuestOrderRequest request, + CancellationToken cancellationToken = default); +} + +public class PublicService : IPublicService +{ + private readonly AppDbContext _db; + private readonly IOrderService _orders; + private readonly IReviewService _reviews; + private readonly IKdsNotifier _kdsNotifier; + private readonly IBranchMenuService _branchMenu; + private readonly IBranchIdentityService _identity; + private readonly IAbuseProtectionService _abuse; + private readonly IHttpContextAccessor _http; + + public PublicService( + AppDbContext db, + IOrderService orders, + IReviewService reviews, + IKdsNotifier kdsNotifier, + IBranchMenuService branchMenu, + IBranchIdentityService identity, + IAbuseProtectionService abuse, + IHttpContextAccessor http) + { + _db = db; + _orders = orders; + _reviews = reviews; + _kdsNotifier = kdsNotifier; + _branchMenu = branchMenu; + _identity = identity; + _abuse = abuse; + _http = http; + } + + public Task> DiscoverAsync( + DiscoverFilterParams filters, + CancellationToken cancellationToken = default) => + _reviews.DiscoverAsync(filters, cancellationToken); + + public async Task GetCafeAsync(string slug, CancellationToken cancellationToken = default) + { + var cafe = await _db.Cafes.FirstOrDefaultAsync(c => c.Slug == slug, cancellationToken); + if (cafe is null) return null; + + var (avg, count) = await _reviews.GetRatingSummaryAsync(cafe.Id, cancellationToken); + + var profile = CafeDiscoverProfileSerializer.Deserialize(cafe.DiscoverProfileJson); + var badges = DiscoverBadgeMapping.ToDtos(cafe) + .Select(b => new CafeBadgePublicDto(b.Key, b.Label, b.Icon)) + .ToList(); + + var gallery = DeserializeStringList(cafe.GalleryJson); + var hours = DeserializeHours(cafe.WorkingHoursJson); + + return new CafePublicDto( + cafe.Id, + cafe.Name, + cafe.NameAr, + cafe.NameEn, + cafe.Slug, + cafe.City, + cafe.Address, + cafe.Phone, + cafe.LogoUrl, + cafe.CoverImageUrl, + cafe.Description, + cafe.IsVerified, + avg, + count, + CafeDiscoverProfileMapping.ToDto(profile), + badges, + gallery, + hours?.IsOpenNow() ?? false, + cafe.InstagramHandle, + cafe.WebsiteUrl, + ToHoursDto(hours)); + } + + private static readonly JsonSerializerOptions _jsonOpts = new() + { + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + + private static IReadOnlyList DeserializeStringList(string? json) + { + if (string.IsNullOrWhiteSpace(json)) return []; + try { return JsonSerializer.Deserialize>(json, _jsonOpts) ?? []; } + catch { return []; } + } + + private static WorkingHoursSchedule? DeserializeHours(string? json) + { + if (string.IsNullOrWhiteSpace(json)) return null; + try { return JsonSerializer.Deserialize(json, _jsonOpts); } + catch { return null; } + } + + private static WorkingHoursPublicDto? ToHoursDto(WorkingHoursSchedule? h) + { + if (h is null) return null; + DaySchedulePublicDto? Map(DaySchedule? d) => + d is null ? null : new DaySchedulePublicDto(d.IsOpen, d.Open, d.Close); + return new WorkingHoursPublicDto(Map(h.Sat), Map(h.Sun), Map(h.Mon), Map(h.Tue), Map(h.Wed), Map(h.Thu), Map(h.Fri)); + } + + public async Task GetMenuAsync(string slug, CancellationToken cancellationToken = default) + { + var cafe = await _db.Cafes.FirstOrDefaultAsync(c => c.Slug == slug, cancellationToken); + if (cafe is null) return null; + + var categories = await _db.MenuCategories + .Where(c => c.CafeId == cafe.Id && c.IsActive) + .OrderBy(c => c.SortOrder) + .ToListAsync(cancellationToken); + + var items = await _db.MenuItems + .Include(i => i.Category) + .Where(i => i.CafeId == cafe.Id && i.IsAvailable) + .ToListAsync(cancellationToken); + + var grouped = categories + .Select(cat => new PublicMenuCategoryDto( + cat.Id, + cat.Name, + cat.NameAr, + cat.NameEn, + cat.Icon, + cat.IconPresetId, + cat.IconStyle, + cat.ImageUrl, + items + .Where(i => i.CategoryId == cat.Id) + .Select(i => new PublicMenuItemDto( + i.Id, + i.CategoryId, + i.Name, + i.NameAr, + i.NameEn, + i.Description, + i.Price, + i.DiscountPercent > 0 ? i.DiscountPercent : cat.DiscountPercent, + MenuItemImageDefaults.ResolveDisplayImageUrl(i), + i.VideoUrl, + i.Model3dUrl, + i.IsAvailable)) + .ToList())) + .Where(c => c.Items.Count > 0) + .ToList(); + + return new PublicMenuDto(cafe.Id, cafe.Name, cafe.Slug, CafeThemeMapping.FromJson(cafe.ThemeJson), grouped); + } + + public async Task<(GuestOrderPlacedDto? Data, string? ErrorCode, string? ErrorMessage)> PlaceOrderAsync( + string slug, + GuestCreateOrderRequest request, + CancellationToken cancellationToken = default) + { + var cafe = await _db.Cafes.FirstOrDefaultAsync(c => c.Slug == slug, cancellationToken); + if (cafe is null) return (null, "NOT_FOUND", "Cafe not found."); + + var traffic = await GuardPublicWriteAsync(cafe, request.CaptchaToken, guestOrder: true, cancellationToken); + if (!traffic.Ok) return (null, traffic.ErrorCode, traffic.Message); + + var maxOrders = PlanLimits.MaxOrdersPerDay(cafe.PlanTier); + if (maxOrders != int.MaxValue) + { + var todayStart = DateTime.UtcNow.Date; + var count = await _db.Orders.CountAsync( + o => o.CafeId == cafe.Id && o.CreatedAt >= todayStart, + cancellationToken); + if (count >= maxOrders) + return (null, "PLAN_LIMIT_REACHED", "This cafe has reached its daily order limit."); + } + + string? couponId = null; + if (!string.IsNullOrWhiteSpace(request.CouponCode)) + { + var coupon = await _db.Coupons.FirstOrDefaultAsync( + c => c.CafeId == cafe.Id && c.Code == request.CouponCode && c.IsActive, + cancellationToken); + couponId = coupon?.Id; + } + + var order = await _orders.CreateGuestOrderAsync( + cafe.Id, + new CreateOrderRequest( + request.OrderType, + null, + request.TableId, + null, + request.GuestName, + request.GuestPhone, + null, + couponId, + request.Items), + request.GuestPhone, + request.GuestName, + cancellationToken); + + if (order is null) + return (null, "VALIDATION_ERROR", "Could not place order. Check menu items and table."); + + return (new GuestOrderPlacedDto(order.Id, order.Status, order.Total, order.TableNumber), null, null); + } + + public async Task TrackOrderAsync( + string orderId, + string trackingToken, + CancellationToken cancellationToken = default) + { + var order = await _db.Orders + .Include(o => o.Items) + .ThenInclude(i => i.MenuItem) + .Include(o => o.Table) + .FirstOrDefaultAsync( + o => o.Id == orderId && o.GuestTrackingToken == trackingToken, + cancellationToken); + + if (order is null) return null; + + return OrderTrackingHelper.BuildTrackDto(order); + } + + public async Task<(ReservationDto? Data, string? ErrorCode, string? Message)> CreateReservationAsync( + string slug, + CreateReservationRequest request, + CancellationToken cancellationToken = default) + { + var cafe = await _db.Cafes.FirstOrDefaultAsync(c => c.Slug == slug, cancellationToken); + if (cafe is null) return (null, "NOT_FOUND", "Cafe not found."); + + var traffic = await GuardPublicWriteAsync(cafe, request.CaptchaToken, guestOrder: false, cancellationToken); + if (!traffic.Ok) return (null, traffic.ErrorCode, traffic.Message); + + var entity = new TableReservation + { + CafeId = cafe.Id, + TableId = request.TableId, + GuestName = request.GuestName, + GuestPhone = request.GuestPhone, + Date = request.Date, + Time = request.Time, + PartySize = request.PartySize, + Notes = request.Notes, + Status = ReservationStatus.Pending + }; + + _db.TableReservations.Add(entity); + await _db.SaveChangesAsync(cancellationToken); + + if (!string.IsNullOrEmpty(entity.TableId)) + await _kdsNotifier.NotifyTableStatusChangedAsync(cafe.Id, cancellationToken); + + var loaded = await _db.TableReservations + .Include(r => r.Table) + .FirstAsync(r => r.Id == entity.Id, cancellationToken); + + return (ToReservationDto(loaded), null, null); + } + + public async Task GetBranchMenuAsync( + string cafeId, + string branchId, + CancellationToken cancellationToken = default) + { + var cafe = await _db.Cafes.FirstOrDefaultAsync(c => c.Id == cafeId, cancellationToken); + if (cafe is null) return null; + + var branchMenu = await _branchMenu.GetBranchMenuAsync( + cafeId, + branchId, + includeUnavailable: false, + cancellationToken); + if (branchMenu is null) return null; + + var categories = await _db.MenuCategories + .Where(c => c.CafeId == cafeId && c.IsActive) + .OrderBy(c => c.SortOrder) + .ToListAsync(cancellationToken); + + var categoryById = categories.ToDictionary(c => c.Id); + var grouped = branchMenu + .Where(i => i.IsAvailable) + .GroupBy(i => i.CategoryId) + .Select(g => + { + categoryById.TryGetValue(g.Key, out var cat); + return new PublicMenuCategoryDto( + g.Key, + cat?.Name ?? "", + cat?.NameAr, + cat?.NameEn, + cat?.Icon, + cat?.IconPresetId, + cat?.IconStyle, + cat?.ImageUrl, + g.Select(i => new PublicMenuItemDto( + i.Id, + i.CategoryId, + i.Name, + i.NameAr, + i.NameEn, + i.Description, + i.EffectivePrice, + i.DiscountPercent, + MenuItemImageDefaults.IsUsableImageUrl(i.ImageUrl) + ? i.ImageUrl! + : MenuItemImageDefaults.ResolveImageUrl(i.Id, i.CategoryId, null), + i.VideoUrl, + i.Model3dUrl, + true)).ToList()); + }) + .Where(c => c.Items.Count > 0) + .OrderBy(c => categoryById.GetValueOrDefault(c.Id)?.SortOrder ?? 0) + .ToList(); + + return new PublicMenuDto(cafe.Id, cafe.Name, cafe.Slug, CafeThemeMapping.FromJson(cafe.ThemeJson), grouped); + } + + public async Task<(GuestQrOrderPlacedDto? Data, string? ErrorCode, string? Message)> PlaceBranchGuestOrderAsync( + string cafeId, + string branchId, + PlaceGuestOrderRequest request, + CancellationToken cancellationToken = default) + { + var cafe = await _db.Cafes.FirstOrDefaultAsync(c => c.Id == cafeId, cancellationToken); + if (cafe is null) return (null, "NOT_FOUND", "Cafe not found."); + + var traffic = await GuardPublicWriteAsync(cafe, request.CaptchaToken, guestOrder: true, cancellationToken); + if (!traffic.Ok) return (null, traffic.ErrorCode, traffic.Message); + + var maxOrders = PlanLimits.MaxOrdersPerDay(cafe.PlanTier); + if (maxOrders != int.MaxValue) + { + var todayStart = DateTime.UtcNow.Date; + var count = await _db.Orders.CountAsync( + o => o.CafeId == cafe.Id && o.CreatedAt >= todayStart, + cancellationToken); + if (count >= maxOrders) + return (null, "PLAN_LIMIT_REACHED", "This cafe has reached its daily order limit."); + } + + return await _orders.PlaceBranchGuestOrderAsync(cafeId, branchId, request, cancellationToken); + } + + private async Task<(bool Ok, string? ErrorCode, string? Message)> GuardPublicWriteAsync( + Cafe cafe, + string? captchaToken, + bool guestOrder, + CancellationToken cancellationToken) + { + var availability = PublicCafeGuard.EnsureAcceptingPublicTraffic(cafe); + if (!availability.Ok) + return (false, availability.ErrorCode, availability.Message); + + var ctx = _http.HttpContext; + if (ctx is null) + return (true, null, null); + + var ip = ClientIpResolver.GetClientIp(ctx); + var writeCheck = await _abuse.CheckPublicWriteByIpAsync(ip, cancellationToken); + if (!writeCheck.Allowed) + return (false, writeCheck.ErrorCode, writeCheck.Message); + + if (guestOrder) + { + var orderCheck = await _abuse.CheckGuestOrderAsync(cafe.Id, ip, cancellationToken); + if (!orderCheck.Allowed) + return (false, orderCheck.ErrorCode, orderCheck.Message); + } + + var captcha = await _abuse.VerifyCaptchaAsync(captchaToken, cancellationToken); + if (!captcha.Ok) + return (false, captcha.ErrorCode, captcha.Message); + + return (true, null, null); + } + + private static ReservationDto ToReservationDto(TableReservation r) => + ReservationService.Map(r); +} diff --git a/src/Meezi.API/Services/QueueService.cs b/src/Meezi.API/Services/QueueService.cs new file mode 100644 index 0000000..98b2e9f --- /dev/null +++ b/src/Meezi.API/Services/QueueService.cs @@ -0,0 +1,220 @@ +using Meezi.API.Models.Queue; +using Meezi.Core.Entities; +using Meezi.Core.Enums; +using Meezi.Core.Interfaces; +using Meezi.Infrastructure.Data; +using Meezi.Infrastructure.Services.Platform; +using Microsoft.EntityFrameworkCore; + +namespace Meezi.API.Services; + +public interface IQueueService +{ + Task GetTodayBoardAsync(string cafeId, string? branchId, CancellationToken ct = default); + Task<(QueueTicketDto? Ticket, string? ErrorCode, string? Message)> IssuePublicAsync( + string cafeId, + PlanTier planTier, + IssueQueueTicketRequest request, + CancellationToken ct = default); + Task<(QueueTicketDto? Ticket, string? Error)> IssueNextAsync( + string cafeId, + string? userId, + IssueQueueTicketRequest request, + CancellationToken ct = default); + Task<(QueueTicketDto? Ticket, string? Error)> UpdateStatusAsync( + string cafeId, + string ticketId, + QueueTicketStatus status, + CancellationToken ct = default); +} + +public class QueueService : IQueueService +{ + private readonly AppDbContext _db; + private readonly IPlatformCatalogService _catalog; + private readonly ISmsService _sms; + + public QueueService(AppDbContext db, IPlatformCatalogService catalog, ISmsService sms) + { + _db = db; + _catalog = catalog; + _sms = sms; + } + + public async Task<(QueueTicketDto? Ticket, string? ErrorCode, string? Message)> IssuePublicAsync( + string cafeId, + PlanTier planTier, + IssueQueueTicketRequest request, + CancellationToken ct = default) + { + if (!await _catalog.IsFeatureEnabledForCafeAsync(cafeId, planTier, "queue", ct)) + return (null, "PLAN_LIMIT_REACHED", "Queue is not available on your plan."); + + var (ticket, error) = await IssueNextAsync(cafeId, null, request, ct); + if (error is not null) + return (null, error, error switch + { + "BRANCH_NOT_FOUND" => "Branch not found.", + "ORDER_NOT_FOUND" => "Order not found.", + _ => "Could not issue ticket." + }); + + return (ticket, null, null); + } + + public async Task GetTodayBoardAsync( + string cafeId, + string? branchId, + CancellationToken ct = default) + { + var today = IranCalendar.TodayInIran; + var tickets = await FilterToday(_db.QueueTickets, cafeId, branchId, today) + .OrderBy(q => q.Number) + .ToListAsync(ct); + + var dtos = tickets.Select(Map).ToList(); + var waiting = tickets.Where(t => t.Status == QueueTicketStatus.Waiting).ToList(); + var nowServing = tickets + .Where(t => t.Status == QueueTicketStatus.Called) + .OrderByDescending(t => t.IssuedAt) + .Select(t => (int?)t.Number) + .FirstOrDefault(); + + if (nowServing is null && waiting.Count > 0) + nowServing = waiting[0].Number; + + var lastIssued = tickets.Count > 0 ? tickets.Max(t => t.Number) : 0; + + return new QueueBoardDto( + today, + nowServing, + lastIssued, + waiting.Count, + dtos); + } + + public async Task<(QueueTicketDto? Ticket, string? Error)> IssueNextAsync( + string cafeId, + string? userId, + IssueQueueTicketRequest request, + CancellationToken ct = default) + { + if (!string.IsNullOrEmpty(request.BranchId)) + { + var branchOk = await _db.Branches.AnyAsync( + b => b.Id == request.BranchId && b.CafeId == cafeId, + ct); + if (!branchOk) return (null, "BRANCH_NOT_FOUND"); + } + + if (!string.IsNullOrEmpty(request.OrderId)) + { + var orderOk = await _db.Orders.AnyAsync( + o => o.Id == request.OrderId && o.CafeId == cafeId, + ct); + if (!orderOk) return (null, "ORDER_NOT_FOUND"); + } + + var today = IranCalendar.TodayInIran; + var branchId = string.IsNullOrEmpty(request.BranchId) ? null : request.BranchId; + + var maxNumber = await FilterToday(_db.QueueTickets, cafeId, branchId, today) + .MaxAsync(q => (int?)q.Number, ct) ?? 0; + + var entity = new QueueTicket + { + Id = $"qt_{Guid.NewGuid():N}"[..24], + CafeId = cafeId, + BranchId = branchId, + ServiceDate = today, + Number = maxNumber + 1, + CustomerLabel = string.IsNullOrWhiteSpace(request.CustomerLabel) + ? null + : request.CustomerLabel.Trim(), + IssuedByUserId = userId, + OrderId = request.OrderId, + Status = QueueTicketStatus.Waiting, + IssuedAt = DateTime.UtcNow + }; + + _db.QueueTickets.Add(entity); + await _db.SaveChangesAsync(ct); + return (Map(entity), null); + } + + public async Task<(QueueTicketDto? Ticket, string? Error)> UpdateStatusAsync( + string cafeId, + string ticketId, + QueueTicketStatus status, + CancellationToken ct = default) + { + var ticket = await _db.QueueTickets.FirstOrDefaultAsync( + q => q.Id == ticketId && q.CafeId == cafeId, + ct); + if (ticket is null) return (null, "NOT_FOUND"); + if (ticket.ServiceDate != IranCalendar.TodayInIran) + return (null, "TICKET_EXPIRED"); + + ticket.Status = status; + await _db.SaveChangesAsync(ct); + + if (status == QueueTicketStatus.Called && !string.IsNullOrWhiteSpace(ticket.CustomerLabel)) + { + var cafe = await _db.Cafes.AsNoTracking() + .Where(c => c.Id == cafeId) + .Select(c => new { c.Name }) + .FirstOrDefaultAsync(ct); + if (cafe is not null) + { + var phone = ExtractPhone(ticket.CustomerLabel); + if (phone is not null) + { + try + { + await _sms.SendMessageAsync( + phone, + $"{cafe.Name}: نوبت شما فرا رسید — شماره {ticket.Number}", + ct); + } + catch + { + /* SMS optional */ + } + } + } + } + + return (Map(ticket), null); + } + + private static string? ExtractPhone(string label) + { + var digits = new string(label.Where(char.IsDigit).ToArray()); + if (digits.Length >= 10 && digits.StartsWith("09", StringComparison.Ordinal)) + return digits.Length > 11 ? digits[^11..] : digits; + return null; + } + + private static IQueryable FilterToday( + IQueryable source, + string cafeId, + string? branchId, + DateOnly today) + { + var query = source.Where(q => q.CafeId == cafeId && q.ServiceDate == today); + return string.IsNullOrEmpty(branchId) + ? query + : query.Where(q => q.BranchId == branchId); + } + + private static QueueTicketDto Map(QueueTicket q) => + new( + q.Id, + q.BranchId, + q.ServiceDate, + q.Number, + q.CustomerLabel, + q.OrderId, + q.Status, + q.IssuedAt); +} diff --git a/src/Meezi.API/Services/RefreshTokenStore.cs b/src/Meezi.API/Services/RefreshTokenStore.cs new file mode 100644 index 0000000..53f7895 --- /dev/null +++ b/src/Meezi.API/Services/RefreshTokenStore.cs @@ -0,0 +1,54 @@ +using System.Text.Json; +using StackExchange.Redis; + +namespace Meezi.API.Services; + +public record RefreshTokenPayload( + string UserId, + string CafeId, + string Role, + string PlanTier, + string Language, + string Actor = Meezi.Core.Constants.MeeziActorKinds.Merchant); + +public interface IRefreshTokenStore +{ + Task StoreAsync(string refreshToken, RefreshTokenPayload payload, TimeSpan ttl, CancellationToken cancellationToken = default); + Task GetAsync(string refreshToken, CancellationToken cancellationToken = default); + Task RevokeAsync(string refreshToken, CancellationToken cancellationToken = default); +} + +public class RedisRefreshTokenStore : IRefreshTokenStore +{ + private readonly IConnectionMultiplexer _redis; + + public RedisRefreshTokenStore(IConnectionMultiplexer redis) + { + _redis = redis; + } + + private static string Key(string token) => $"refresh:{token}"; + + public async Task StoreAsync(string refreshToken, RefreshTokenPayload payload, TimeSpan ttl, CancellationToken cancellationToken = default) + { + var db = _redis.GetDatabase(); + var json = JsonSerializer.Serialize(payload); + await db.StringSetAsync(Key(refreshToken), json, ttl); + } + + public async Task GetAsync(string refreshToken, CancellationToken cancellationToken = default) + { + var db = _redis.GetDatabase(); + var value = await db.StringGetAsync(Key(refreshToken)); + if (value.IsNullOrEmpty) + return null; + + return JsonSerializer.Deserialize(value.ToString()); + } + + public async Task RevokeAsync(string refreshToken, CancellationToken cancellationToken = default) + { + var db = _redis.GetDatabase(); + await db.KeyDeleteAsync(Key(refreshToken)); + } +} diff --git a/src/Meezi.API/Services/ReportPlanGate.cs b/src/Meezi.API/Services/ReportPlanGate.cs new file mode 100644 index 0000000..0982438 --- /dev/null +++ b/src/Meezi.API/Services/ReportPlanGate.cs @@ -0,0 +1,51 @@ +using Meezi.Core.Constants; +using Meezi.Core.Enums; + +namespace Meezi.API.Services; + +public static class ReportPlanGate +{ + public static bool IsDateInRange(PlanTier tier, DateOnly date, DateOnly todayIran) + { + var maxDays = PlanLimits.MaxReportHistoryDays(tier); + if (maxDays == int.MaxValue) + return date <= todayIran; + + var earliest = todayIran.AddDays(-(maxDays - 1)); + return date >= earliest && date <= todayIran; + } + + public static (DateOnly From, DateOnly To)? ClampRange( + PlanTier tier, + DateOnly from, + DateOnly to, + DateOnly todayIran) + { + if (from > to) return null; + if (!IsDateInRange(tier, to, todayIran) || !IsDateInRange(tier, from, todayIran)) + return null; + + var maxDays = PlanLimits.MaxReportHistoryDays(tier); + if (maxDays == int.MaxValue) + return (from, to); + + var earliest = todayIran.AddDays(-(maxDays - 1)); + var clampedFrom = from < earliest ? earliest : from; + var clampedTo = to > todayIran ? todayIran : to; + if (clampedFrom > clampedTo) return null; + return (clampedFrom, clampedTo); + } + + public static string LimitMessage(PlanTier tier) + { + var days = PlanLimits.MaxReportHistoryDays(tier); + return tier switch + { + PlanTier.Free => + "Daily reports on the Free plan are limited to today and the previous 7 days. Upgrade to Pro for 90 days of history.", + PlanTier.Pro => + "Daily reports on the Pro plan are limited to the last 90 days. Upgrade to Business for unlimited history.", + _ => "Report date is outside your plan range." + }; + } +} diff --git a/src/Meezi.API/Services/ReportService.cs b/src/Meezi.API/Services/ReportService.cs new file mode 100644 index 0000000..6f741cc --- /dev/null +++ b/src/Meezi.API/Services/ReportService.cs @@ -0,0 +1,312 @@ +using System.Globalization; +using Meezi.API.Models.Reports; +using Meezi.API.Utils; +using Meezi.Core.Enums; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using OfficeOpenXml; + +namespace Meezi.API.Services; + +public interface IReportService +{ + Task GetDailyReportAsync(string cafeId, string dateJalali, CancellationToken cancellationToken = default); + Task GetMonthlyReportAsync(string cafeId, string monthJalali, CancellationToken cancellationToken = default); + Task> GetTrendAsync(string cafeId, int days, CancellationToken cancellationToken = default); + Task ExportExcelAsync(string cafeId, string monthJalali, CancellationToken cancellationToken = default); +} + +public class ReportService : IReportService +{ + private static readonly OrderStatus[] RevenueStatuses = + [ + OrderStatus.Confirmed, + OrderStatus.Preparing, + OrderStatus.Ready, + OrderStatus.Delivered + ]; + + private readonly AppDbContext _db; + + public ReportService(AppDbContext db) => _db = db; + + public async Task GetDailyReportAsync( + string cafeId, + string dateJalali, + CancellationToken cancellationToken = default) + { + if (!JalaliCalendarHelper.TryParseJalaliDate(dateJalali, out var y, out var m, out var d)) + { + var today = JalaliCalendarHelper.TodayJalali(); + y = today.Year; + m = today.Month; + d = today.Day; + dateJalali = JalaliCalendarHelper.FormatJalaliDate(y, m, d); + } + + var (utcStart, utcEnd) = JalaliCalendarHelper.GetUtcRangeForJalaliDay(y, m, d); + var orders = await LoadRevenueOrdersAsync(cafeId, utcStart, utcEnd, cancellationToken); + + var newCustomers = await _db.Customers + .Where(c => c.CafeId == cafeId && c.CreatedAt >= utcStart && c.CreatedAt < utcEnd) + .CountAsync(cancellationToken); + + var returningCustomers = await CountReturningCustomersAsync(cafeId, utcStart, utcEnd, cancellationToken); + var topItems = await GetTopItemsAsync(cafeId, utcStart, utcEnd, 5, cancellationToken); + + return new DailyReportDto( + dateJalali, + orders.Count, + newCustomers, + returningCustomers, + orders.Sum(o => o.Total), + orders.Sum(o => o.TaxTotal), + orders.Sum(o => o.DiscountAmount), + topItems); + } + + public async Task GetMonthlyReportAsync( + string cafeId, + string monthJalali, + CancellationToken cancellationToken = default) + { + if (!JalaliCalendarHelper.TryParseJalaliMonth(monthJalali, out var y, out var m)) + { + var today = JalaliCalendarHelper.TodayJalali(); + y = today.Year; + m = today.Month; + monthJalali = JalaliCalendarHelper.FormatJalaliMonth(y, m); + } + + var (utcStart, utcEnd) = JalaliCalendarHelper.GetUtcRangeForJalaliMonth(y, m); + var daysInMonth = new PersianCalendar().GetDaysInMonth(y, m); + var salaryCosts = await GetSalaryCostsForGregorianMonthAsync(cafeId, utcStart, cancellationToken); + var dailyCost = daysInMonth > 0 ? salaryCosts / daysInMonth : 0m; + + var breakdown = new List(); + decimal totalRevenue = 0; + + for (var day = 1; day <= daysInMonth; day++) + { + var (dayStart, dayEnd) = JalaliCalendarHelper.GetUtcRangeForJalaliDay(y, m, day); + var dayOrders = await LoadRevenueOrdersAsync(cafeId, dayStart, dayEnd, cancellationToken); + var revenue = dayOrders.Sum(o => o.Total); + totalRevenue += revenue; + breakdown.Add(new DailyBreakdownDto( + JalaliCalendarHelper.FormatJalaliDate(y, m, day), + revenue, + dailyCost)); + } + + var totalCosts = salaryCosts; + return new MonthlyReportDto( + monthJalali, + breakdown, + totalRevenue, + totalCosts, + salaryCosts, + 0m, + totalRevenue - totalCosts); + } + + public async Task> GetTrendAsync( + string cafeId, + int days, + CancellationToken cancellationToken = default) + { + days = Math.Clamp(days, 1, 31); + var (y, m, d) = JalaliCalendarHelper.TodayJalali(); + var persian = new PersianCalendar(); + var cursor = persian.ToDateTime(y, m, d, 0, 0, 0, 0); + var monthYear = $"{cursor:yyyy-MM}"; + var salaryCosts = await _db.EmployeeSalaries + .Include(s => s.Employee) + .Where(s => s.Employee.CafeId == cafeId && s.MonthYear == monthYear) + .SumAsync(s => s.NetSalary, cancellationToken); + var daysInMonth = persian.GetDaysInMonth(y, m); + var dailyCost = daysInMonth > 0 ? salaryCosts / daysInMonth : 0m; + + var result = new List(); + for (var i = days - 1; i >= 0; i--) + { + var localDay = cursor.AddDays(-i); + var jy = persian.GetYear(localDay); + var jm = persian.GetMonth(localDay); + var jd = persian.GetDayOfMonth(localDay); + var (utcStart, utcEnd) = JalaliCalendarHelper.GetUtcRangeForJalaliDay(jy, jm, jd); + var orders = await LoadRevenueOrdersAsync(cafeId, utcStart, utcEnd, cancellationToken); + result.Add(new TrendDayDto( + JalaliCalendarHelper.FormatJalaliDate(jy, jm, jd), + orders.Sum(o => o.Total), + dailyCost)); + } + + return result; + } + + public async Task ExportExcelAsync( + string cafeId, + string monthJalali, + CancellationToken cancellationToken = default) + { + if (!JalaliCalendarHelper.TryParseJalaliMonth(monthJalali, out var y, out var m)) + throw new ArgumentException("Invalid Jalali month.", nameof(monthJalali)); + + ExcelPackage.LicenseContext = LicenseContext.NonCommercial; + var (utcStart, utcEnd) = JalaliCalendarHelper.GetUtcRangeForJalaliMonth(y, m); + + var orders = await _db.Orders + .Include(o => o.Items) + .ThenInclude(i => i.MenuItem) + .Include(o => o.Customer) + .Where(o => o.CafeId == cafeId && RevenueStatuses.Contains(o.Status)) + .Where(o => o.CreatedAt >= utcStart && o.CreatedAt < utcEnd) + .OrderBy(o => o.CreatedAt) + .ToListAsync(cancellationToken); + + var customers = await _db.Customers + .Where(c => c.CafeId == cafeId && c.CreatedAt >= utcStart && c.CreatedAt < utcEnd) + .ToListAsync(cancellationToken); + + var salaries = await _db.EmployeeSalaries + .Include(s => s.Employee) + .Where(s => s.Employee.CafeId == cafeId && s.MonthYear == $"{utcStart:yyyy-MM}") + .ToListAsync(cancellationToken); + + using var package = new ExcelPackage(); + + var sales = package.Workbook.Worksheets.Add("Sales"); + sales.Cells[1, 1].Value = "OrderId"; + sales.Cells[1, 2].Value = "Date"; + sales.Cells[1, 3].Value = "Total"; + sales.Cells[1, 4].Value = "Tax"; + sales.Cells[1, 5].Value = "Discount"; + var row = 2; + foreach (var order in orders) + { + var jalali = JalaliCalendarHelper.ToJalali(order.CreatedAt); + sales.Cells[row, 1].Value = order.Id; + sales.Cells[row, 2].Value = JalaliCalendarHelper.FormatJalaliDate(jalali.Year, jalali.Month, jalali.Day); + sales.Cells[row, 3].Value = order.Total; + sales.Cells[row, 4].Value = order.TaxTotal; + sales.Cells[row, 5].Value = order.DiscountAmount; + row++; + } + + var items = package.Workbook.Worksheets.Add("Items"); + items.Cells[1, 1].Value = "Item"; + items.Cells[1, 2].Value = "Quantity"; + items.Cells[1, 3].Value = "Revenue"; + row = 2; + foreach (var line in orders.SelectMany(o => o.Items)) + { + items.Cells[row, 1].Value = line.MenuItem.Name; + items.Cells[row, 2].Value = line.Quantity; + items.Cells[row, 3].Value = line.UnitPrice * line.Quantity; + row++; + } + + var customersSheet = package.Workbook.Worksheets.Add("Customers"); + customersSheet.Cells[1, 1].Value = "Name"; + customersSheet.Cells[1, 2].Value = "Phone"; + customersSheet.Cells[1, 3].Value = "Created"; + row = 2; + foreach (var customer in customers) + { + var jalali = JalaliCalendarHelper.ToJalali(customer.CreatedAt); + customersSheet.Cells[row, 1].Value = customer.Name; + customersSheet.Cells[row, 2].Value = customer.Phone; + customersSheet.Cells[row, 3].Value = JalaliCalendarHelper.FormatJalaliDate(jalali.Year, jalali.Month, jalali.Day); + row++; + } + + var employeesSheet = package.Workbook.Worksheets.Add("Employees"); + employeesSheet.Cells[1, 1].Value = "Employee"; + employeesSheet.Cells[1, 2].Value = "Month"; + employeesSheet.Cells[1, 3].Value = "NetSalary"; + employeesSheet.Cells[1, 4].Value = "Paid"; + row = 2; + foreach (var salary in salaries) + { + employeesSheet.Cells[row, 1].Value = salary.Employee.Name; + employeesSheet.Cells[row, 2].Value = salary.MonthYear; + employeesSheet.Cells[row, 3].Value = salary.NetSalary; + employeesSheet.Cells[row, 4].Value = salary.IsPaid ? "Yes" : "No"; + row++; + } + + return await package.GetAsByteArrayAsync(cancellationToken); + } + + private async Task> LoadRevenueOrdersAsync( + string cafeId, + DateTime utcStart, + DateTime utcEnd, + CancellationToken cancellationToken) + { + return await _db.Orders + .Where(o => o.CafeId == cafeId && RevenueStatuses.Contains(o.Status)) + .Where(o => o.CreatedAt >= utcStart && o.CreatedAt < utcEnd) + .ToListAsync(cancellationToken); + } + + private async Task CountReturningCustomersAsync( + string cafeId, + DateTime utcStart, + DateTime utcEnd, + CancellationToken cancellationToken) + { + var customerIds = await _db.Orders + .Where(o => o.CafeId == cafeId && o.CustomerId != null) + .Where(o => RevenueStatuses.Contains(o.Status)) + .Where(o => o.CreatedAt >= utcStart && o.CreatedAt < utcEnd) + .Select(o => o.CustomerId!) + .Distinct() + .ToListAsync(cancellationToken); + + if (customerIds.Count == 0) + return 0; + + return await _db.Customers + .Where(c => c.CafeId == cafeId && customerIds.Contains(c.Id) && c.CreatedAt < utcStart) + .CountAsync(cancellationToken); + } + + private async Task> GetTopItemsAsync( + string cafeId, + DateTime utcStart, + DateTime utcEnd, + int take, + CancellationToken cancellationToken) + { + var lines = await _db.OrderItems + .Include(i => i.MenuItem) + .Include(i => i.Order) + .Where(i => i.Order.CafeId == cafeId && RevenueStatuses.Contains(i.Order.Status)) + .Where(i => i.Order.CreatedAt >= utcStart && i.Order.CreatedAt < utcEnd) + .ToListAsync(cancellationToken); + + return lines + .GroupBy(i => i.MenuItemId) + .Select(g => new TopItemDto( + g.Key, + g.First().MenuItem.Name, + g.Sum(x => x.Quantity), + g.Sum(x => x.UnitPrice * x.Quantity))) + .OrderByDescending(x => x.Revenue) + .Take(take) + .ToList(); + } + + private async Task GetSalaryCostsForGregorianMonthAsync( + string cafeId, + DateTime utcStart, + CancellationToken cancellationToken) + { + var monthYear = utcStart.ToString("yyyy-MM"); + return await _db.EmployeeSalaries + .Include(s => s.Employee) + .Where(s => s.Employee.CafeId == cafeId && s.MonthYear == monthYear) + .SumAsync(s => s.NetSalary, cancellationToken); + } +} diff --git a/src/Meezi.API/Services/ReservationService.cs b/src/Meezi.API/Services/ReservationService.cs new file mode 100644 index 0000000..b013acc --- /dev/null +++ b/src/Meezi.API/Services/ReservationService.cs @@ -0,0 +1,133 @@ +using Meezi.API.Models.Public; +using Meezi.Core.Entities; +using Meezi.Core.Enums; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; + +namespace Meezi.API.Services; + +public interface IReservationService +{ + Task> GetReservationsAsync( + string cafeId, + DateOnly? date, + ReservationStatus? status, + CancellationToken cancellationToken = default); + + Task CreateAsync( + string cafeId, + CreateReservationRequest request, + CancellationToken cancellationToken = default); + + Task UpdateStatusAsync( + string cafeId, + string reservationId, + ReservationStatus status, + CancellationToken cancellationToken = default); +} + +public class ReservationService : IReservationService +{ + private readonly AppDbContext _db; + private readonly IKdsNotifier _kdsNotifier; + + public ReservationService(AppDbContext db, IKdsNotifier kdsNotifier) + { + _db = db; + _kdsNotifier = kdsNotifier; + } + + public async Task> GetReservationsAsync( + string cafeId, + DateOnly? date, + ReservationStatus? status, + CancellationToken cancellationToken = default) + { + var query = _db.TableReservations + .Include(r => r.Table) + .Where(r => r.CafeId == cafeId); + if (date.HasValue) + query = query.Where(r => r.Date == date.Value); + if (status.HasValue) + query = query.Where(r => r.Status == status.Value); + + var list = await query + .OrderBy(r => r.Date) + .ThenBy(r => r.Time) + .ToListAsync(cancellationToken); + + return list.Select(Map).ToList(); + } + + public async Task CreateAsync( + string cafeId, + CreateReservationRequest request, + CancellationToken cancellationToken = default) + { + if (!string.IsNullOrEmpty(request.TableId)) + { + var tableOk = await _db.Tables.AnyAsync( + t => t.Id == request.TableId && t.CafeId == cafeId, + cancellationToken); + if (!tableOk) return null; + } + + var entity = new TableReservation + { + CafeId = cafeId, + TableId = request.TableId, + GuestName = request.GuestName.Trim(), + GuestPhone = request.GuestPhone.Trim(), + Date = request.Date, + Time = request.Time, + PartySize = request.PartySize, + Notes = request.Notes, + Status = ReservationStatus.Confirmed + }; + + _db.TableReservations.Add(entity); + await _db.SaveChangesAsync(cancellationToken); + + if (!string.IsNullOrEmpty(entity.TableId)) + await _kdsNotifier.NotifyTableStatusChangedAsync(cafeId, cancellationToken); + + var loaded = await _db.TableReservations + .Include(r => r.Table) + .FirstAsync(r => r.Id == entity.Id, cancellationToken); + + return Map(loaded); + } + + public async Task UpdateStatusAsync( + string cafeId, + string reservationId, + ReservationStatus status, + CancellationToken cancellationToken = default) + { + var entity = await _db.TableReservations + .Include(r => r.Table) + .FirstOrDefaultAsync(r => r.Id == reservationId && r.CafeId == cafeId, cancellationToken); + if (entity is null) return null; + + entity.Status = status; + await _db.SaveChangesAsync(cancellationToken); + + if (!string.IsNullOrEmpty(entity.TableId)) + await _kdsNotifier.NotifyTableStatusChangedAsync(cafeId, cancellationToken); + + return Map(entity); + } + + internal static ReservationDto Map(TableReservation r) => new( + r.Id, + r.CafeId, + r.TableId, + r.Table?.Number, + r.GuestName, + r.GuestPhone, + r.Date, + r.Time, + r.PartySize, + r.Status, + r.Notes); +} diff --git a/src/Meezi.API/Services/ReviewService.cs b/src/Meezi.API/Services/ReviewService.cs new file mode 100644 index 0000000..4a78c2b --- /dev/null +++ b/src/Meezi.API/Services/ReviewService.cs @@ -0,0 +1,358 @@ +using System.Text.Json; +using Meezi.API.Models.Public; +using Meezi.API.Security; +using Meezi.Core.Discover; +using Meezi.Core.Entities; +using Meezi.Core.Utilities; +using Meezi.Infrastructure.Data; +using Meezi.Core.Enums; +using Meezi.Infrastructure.Discover; +using Microsoft.EntityFrameworkCore; + +namespace Meezi.API.Services; + +public interface IReviewService +{ + Task> DiscoverAsync( + DiscoverFilterParams filters, + CancellationToken cancellationToken = default); + + Task> GetReviewsAsync( + string cafeId, + int page, + int pageSize, + bool publicOnly = true, + CancellationToken cancellationToken = default); + Task<(CafeReviewDto? Data, string? ErrorCode, string? Message)> CreateReviewAsync( + string cafeId, + CreateCafeReviewRequest request, + CancellationToken cancellationToken = default); + Task ReplyReviewAsync(string cafeId, string reviewId, string reply, CancellationToken cancellationToken = default); + Task SetHiddenAsync(string cafeId, string reviewId, bool isHidden, CancellationToken cancellationToken = default); + Task<(CafeReviewDto? Data, string? ErrorCode, string? Message)> CreateReviewWithPhotosAsync( + string cafeId, + CreateCafeReviewRequest request, + IReadOnlyList photos, + CancellationToken cancellationToken = default); + Task<(double Average, int Count)> GetRatingSummaryAsync(string cafeId, CancellationToken cancellationToken = default); +} + +public class ReviewService : IReviewService +{ + private const int MaxReviewPhotos = 3; + + private readonly AppDbContext _db; + private readonly IAbuseProtectionService _abuse; + private readonly IHttpContextAccessor _http; + private readonly IMediaStorageService _media; + + public ReviewService( + AppDbContext db, + IAbuseProtectionService abuse, + IHttpContextAccessor http, + IMediaStorageService media) + { + _db = db; + _abuse = abuse; + _http = http; + _media = media; + } + + public async Task> DiscoverAsync( + DiscoverFilterParams filters, + CancellationToken cancellationToken = default) + { + var query = _db.Cafes.Where(c => c.IsVerified && c.DeletedAt == null); + + if (!string.IsNullOrWhiteSpace(filters.City)) + query = query.Where(c => c.City != null && c.City.Contains(filters.City)); + + if (!string.IsNullOrWhiteSpace(filters.Q)) + { + var q = filters.Q.Trim(); + var qNorm = PersianSearchNormalizer.Normalize(q); + var pattern = $"%{q}%"; + var patternNorm = qNorm.Length > 0 && !string.Equals(qNorm, q, StringComparison.Ordinal) + ? $"%{qNorm}%" + : null; + + query = query.Where(c => + EF.Functions.ILike(c.Name, pattern) + || EF.Functions.ILike(c.Slug, pattern) + || (c.Description != null && EF.Functions.ILike(c.Description, pattern)) + || (c.Address != null && EF.Functions.ILike(c.Address, pattern)) + || (c.City != null && EF.Functions.ILike(c.City, pattern)) + || (c.NameAr != null && EF.Functions.ILike(c.NameAr, pattern)) + || (patternNorm != null && ( + EF.Functions.ILike(c.Name, patternNorm) + || (c.Description != null && EF.Functions.ILike(c.Description, patternNorm)) + || (c.Address != null && EF.Functions.ILike(c.Address, patternNorm)))) + || _db.MenuItems.Any(m => + m.CafeId == c.Id + && m.DeletedAt == null + && (EF.Functions.ILike(m.Name, pattern) + || (patternNorm != null && EF.Functions.ILike(m.Name, patternNorm))))); + } + + var cafes = await query.ToListAsync(cancellationToken); + var cafeIds = cafes.Select(c => c.Id).ToList(); + + var ratings = await _db.CafeReviews + .Where(r => cafeIds.Contains(r.CafeId) && !r.IsHidden) + .GroupBy(r => r.CafeId) + .Select(g => new { CafeId = g.Key, Avg = g.Average(x => x.Rating), Count = g.Count() }) + .ToListAsync(cancellationToken); + + var ratingMap = ratings.ToDictionary(x => x.CafeId, x => (x.Avg, x.Count)); + + // Determine whether this is a free-text NLP search or a pure chip-filter search + bool hasTextQuery = !string.IsNullOrWhiteSpace(filters.Q); + + var result = cafes + .Select(c => + { + ratingMap.TryGetValue(c.Id, out var r); + var count = r.Count; + var avg = count > 0 ? r.Avg : 0.0; + var profile = CafeDiscoverProfileSerializer.Deserialize(c.DiscoverProfileJson); + var hours = DeserializeHours(c.WorkingHoursJson); + var gallery = DeserializeGallery(c.GalleryJson); + var badges = MapBadges(c); + + // openNow filter — skip cafes that are provably closed + if (filters.OpenNow && hours is not null && !hours.IsOpenNow()) + return default; + + double score; + if (hasTextQuery) + { + // Soft scoring: partial matches surface instead of being hidden + score = DiscoverProfileMatcher.Score(profile, filters); + if (filters.RequireProfile + && !DiscoverProfileMatcher.HasMeaningfulProfile(profile) + && score < DiscoverProfileMatcher.MinScoreThreshold) + return default; + if (score < DiscoverProfileMatcher.MinScoreThreshold) + return default; + } + else + { + // Hard AND match for chip-only searches (backward compatible) + if (!DiscoverProfileMatcher.Matches(profile, filters)) + return default; + score = DiscoverProfileMatcher.Score(profile, filters); + } + + bool isOpenNow = hours?.IsOpenNow() ?? false; + + var dto = new CafeDiscoverDto( + c.Id, + c.Name, + c.Slug, + c.City, + c.Address, + c.LogoUrl, + c.CoverImageUrl, + c.IsVerified, + Math.Round(avg, 1), + count, + CafeDiscoverProfileMapping.ToDto(profile), + badges, + gallery, + isOpenNow, + c.InstagramHandle, + c.WebsiteUrl, + score); + return (dto, score, (object?)dto); + }) + .Where(x => x.Item3 is not null) + .Select(x => x.dto) + .ToList(); + + if (filters.MinRating.HasValue) + result = result.Where(c => c.AverageRating >= filters.MinRating.Value).ToList(); + + result = (filters.Sort?.ToLowerInvariant()) switch + { + "rating" => result.OrderByDescending(c => c.AverageRating).ThenByDescending(c => c.ReviewCount).ToList(), + "reviews" => result.OrderByDescending(c => c.ReviewCount).ToList(), + "score" => result.OrderByDescending(c => c.RelevanceScore).ThenByDescending(c => c.AverageRating).ToList(), + _ => hasTextQuery + ? result.OrderByDescending(c => c.RelevanceScore).ThenByDescending(c => c.AverageRating).ToList() + : result.OrderBy(c => c.Name).ToList() + }; + + return result; + } + + public async Task> GetReviewsAsync( + string cafeId, + int page, + int pageSize, + bool publicOnly = true, + CancellationToken cancellationToken = default) + { + page = Math.Max(1, page); + pageSize = Math.Clamp(pageSize, 1, 50); + + var reviews = await _db.CafeReviews + .Include(r => r.Photos) + .Where(r => r.CafeId == cafeId && (!publicOnly || !r.IsHidden)) + .OrderByDescending(r => r.CreatedAt) + .Skip((page - 1) * pageSize) + .Take(pageSize) + .ToListAsync(cancellationToken); + + return reviews.Select(r => ToDto(r, publicView: true)).ToList(); + } + + public async Task<(CafeReviewDto? Data, string? ErrorCode, string? Message)> CreateReviewAsync( + string cafeId, + CreateCafeReviewRequest request, + CancellationToken cancellationToken = default) + { + var cafe = await _db.Cafes.FirstOrDefaultAsync(c => c.Id == cafeId && c.IsVerified, cancellationToken); + if (cafe is null) return (null, "NOT_FOUND", "Cafe not found."); + + var ctx = _http.HttpContext; + if (ctx is not null) + { + var availability = PublicCafeGuard.EnsureAcceptingPublicTraffic(cafe); + if (!availability.Ok) return (null, availability.ErrorCode, availability.Message); + + var ip = ClientIpResolver.GetClientIp(ctx); + var writeCheck = await _abuse.CheckPublicWriteByIpAsync(ip, cancellationToken); + if (!writeCheck.Allowed) return (null, writeCheck.ErrorCode, writeCheck.Message); + + var captcha = await _abuse.VerifyCaptchaAsync(request.CaptchaToken, cancellationToken); + if (!captcha.Ok) return (null, captcha.ErrorCode, captcha.Message); + } + + var entity = new CafeReview + { + CafeId = cafeId, + AuthorName = request.AuthorName.Trim(), + AuthorPhone = request.AuthorPhone?.Trim(), + Rating = request.Rating, + Comment = request.Comment?.Trim() + }; + + _db.CafeReviews.Add(entity); + await _db.SaveChangesAsync(cancellationToken); + return (ToDto(entity, publicView: true), null, null); + } + + public async Task<(CafeReviewDto? Data, string? ErrorCode, string? Message)> CreateReviewWithPhotosAsync( + string cafeId, + CreateCafeReviewRequest request, + IReadOnlyList photos, + CancellationToken cancellationToken = default) + { + var baseResult = await CreateReviewAsync(cafeId, request, cancellationToken); + if (baseResult.Data is null) + return baseResult; + + var files = photos?.Where(f => f.Length > 0).Take(MaxReviewPhotos).ToList() ?? []; + if (files.Count == 0) + return baseResult; + + var review = await _db.CafeReviews + .Include(r => r.Photos) + .FirstAsync(r => r.Id == baseResult.Data.Id, cancellationToken); + + var sort = 0; + foreach (var file in files) + { + var url = await _media.SaveReviewPhotoAsync(cafeId, file, cancellationToken); + if (url is null) continue; + review.Photos.Add(new CafeReviewPhoto + { + ReviewId = review.Id, + Url = url, + SortOrder = sort++ + }); + } + + await _db.SaveChangesAsync(cancellationToken); + return (ToDto(review, publicView: true), null, null); + } + + public async Task ReplyReviewAsync( + string cafeId, + string reviewId, + string reply, + CancellationToken cancellationToken = default) + { + var entity = await _db.CafeReviews + .FirstOrDefaultAsync(r => r.Id == reviewId && r.CafeId == cafeId, cancellationToken); + if (entity is null) return null; + + entity.OwnerReply = reply.Trim(); + entity.OwnerRepliedAt = DateTime.UtcNow; + await _db.SaveChangesAsync(cancellationToken); + return ToDto(entity, publicView: false); + } + + public async Task SetHiddenAsync( + string cafeId, + string reviewId, + bool isHidden, + CancellationToken cancellationToken = default) + { + var entity = await _db.CafeReviews + .Include(r => r.Photos) + .FirstOrDefaultAsync(r => r.Id == reviewId && r.CafeId == cafeId, cancellationToken); + if (entity is null) return null; + + entity.IsHidden = isHidden; + await _db.SaveChangesAsync(cancellationToken); + return ToDto(entity, publicView: false); + } + + public async Task<(double Average, int Count)> GetRatingSummaryAsync(string cafeId, CancellationToken cancellationToken = default) + { + var reviews = await _db.CafeReviews + .Where(r => r.CafeId == cafeId && !r.IsHidden) + .ToListAsync(cancellationToken); + if (reviews.Count == 0) return (0, 0); + return (Math.Round(reviews.Average(r => r.Rating), 1), reviews.Count); + } + + private static IReadOnlyList MapBadges(Cafe c) => + DiscoverBadgeMapping.ToDtos(c) + .Select(b => new CafeBadgePublicDto(b.Key, b.Label, b.Icon)) + .ToList(); + + private static readonly JsonSerializerOptions _jsonOpts = new() + { + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase + }; + + private static WorkingHoursSchedule? DeserializeHours(string? json) + { + if (string.IsNullOrWhiteSpace(json)) return null; + try { return JsonSerializer.Deserialize(json, _jsonOpts); } + catch { return null; } + } + + private static IReadOnlyList DeserializeGallery(string? json) + { + if (string.IsNullOrWhiteSpace(json)) return []; + try + { + return JsonSerializer.Deserialize>(json, _jsonOpts) ?? []; + } + catch { return []; } + } + + private static CafeReviewDto ToDto(CafeReview r, bool publicView) => new( + r.Id, + r.AuthorName, + r.Rating, + r.Comment, + r.OwnerReply, + r.CreatedAt, + r.Photos.OrderBy(p => p.SortOrder).Select(p => p.Url).ToList(), + publicView ? false : r.IsHidden); +} diff --git a/src/Meezi.API/Services/ShiftService.cs b/src/Meezi.API/Services/ShiftService.cs new file mode 100644 index 0000000..0e14c3c --- /dev/null +++ b/src/Meezi.API/Services/ShiftService.cs @@ -0,0 +1,263 @@ +using Meezi.API.Models.Shifts; +using Meezi.Core.Entities; +using Meezi.Core.Enums; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; + +namespace Meezi.API.Services; + +public record ShiftServiceResult(bool Success, T? Data, string? ErrorCode = null, string? Field = null); + +public interface IShiftService +{ + Task> OpenShiftAsync( + string cafeId, + string branchId, + decimal openingCash, + string userId, + CancellationToken cancellationToken = default); + + Task> CloseShiftAsync( + string cafeId, + string shiftId, + decimal closingCash, + string userId, + CancellationToken cancellationToken = default); + + Task GetCurrentShiftAsync( + string cafeId, + string branchId, + CancellationToken cancellationToken = default); + + Task?> GetTransactionsAsync( + string cafeId, + string shiftId, + CancellationToken cancellationToken = default); + + Task> RecordTransactionAsync( + string cafeId, + string shiftId, + CashTransactionType type, + PaymentMethod method, + decimal amount, + string createdByUserId, + string? referenceId = null, + string? note = null, + CancellationToken cancellationToken = default); + + Task> RequireOpenShiftForBranchAsync( + string cafeId, + string branchId, + CancellationToken cancellationToken = default); +} + +public class ShiftService : IShiftService +{ + private readonly AppDbContext _db; + + public ShiftService(AppDbContext db) => _db = db; + + public async Task> OpenShiftAsync( + string cafeId, + string branchId, + decimal openingCash, + string userId, + CancellationToken cancellationToken = default) + { + var branch = await _db.Branches.FirstOrDefaultAsync( + b => b.Id == branchId && b.CafeId == cafeId && b.IsActive, + cancellationToken); + if (branch is null) + return new ShiftServiceResult(false, null, "BRANCH_NOT_FOUND", "branchId"); + + var hasOpen = await _db.RegisterShifts.AnyAsync( + s => s.BranchId == branchId && s.CafeId == cafeId && s.Status == ShiftStatus.Open, + cancellationToken); + if (hasOpen) + return new ShiftServiceResult(false, null, "SHIFT_ALREADY_OPEN", "branchId"); + + var employeeExists = await _db.Employees.AnyAsync( + e => e.Id == userId && e.CafeId == cafeId, + cancellationToken); + if (!employeeExists) + return new ShiftServiceResult(false, null, "USER_NOT_FOUND", "userId"); + + var now = DateTime.UtcNow; + var shift = new Shift + { + CafeId = cafeId, + BranchId = branchId, + OpenedByUserId = userId, + OpenedAt = now, + OpeningCash = openingCash, + ExpectedCash = openingCash, + Status = ShiftStatus.Open, + CreatedAt = now + }; + + _db.RegisterShifts.Add(shift); + await _db.SaveChangesAsync(cancellationToken); + + return new ShiftServiceResult(true, ToDto(shift)); + } + + public async Task> CloseShiftAsync( + string cafeId, + string shiftId, + decimal closingCash, + string userId, + CancellationToken cancellationToken = default) + { + var shift = await _db.RegisterShifts + .Include(s => s.Transactions) + .FirstOrDefaultAsync(s => s.Id == shiftId && s.CafeId == cafeId, cancellationToken); + + if (shift is null) + return new ShiftServiceResult(false, null, "SHIFT_NOT_FOUND"); + + if (shift.Status != ShiftStatus.Open) + return new ShiftServiceResult(false, null, "SHIFT_ALREADY_CLOSED"); + + shift.ExpectedCash = CalculateExpectedCash(shift.OpeningCash, shift.Transactions); + shift.ClosingCash = closingCash; + shift.Discrepancy = closingCash - shift.ExpectedCash; + shift.ClosedByUserId = userId; + shift.ClosedAt = DateTime.UtcNow; + shift.Status = ShiftStatus.Closed; + + await _db.SaveChangesAsync(cancellationToken); + + return new ShiftServiceResult(true, ToDto(shift)); + } + + public async Task GetCurrentShiftAsync( + string cafeId, + string branchId, + CancellationToken cancellationToken = default) + { + var shift = await _db.RegisterShifts + .AsNoTracking() + .FirstOrDefaultAsync( + s => s.CafeId == cafeId && s.BranchId == branchId && s.Status == ShiftStatus.Open, + cancellationToken); + + return shift is null ? null : ToDto(shift); + } + + public async Task?> GetTransactionsAsync( + string cafeId, + string shiftId, + CancellationToken cancellationToken = default) + { + var shiftExists = await _db.RegisterShifts.AnyAsync( + s => s.Id == shiftId && s.CafeId == cafeId, + cancellationToken); + if (!shiftExists) return null; + + var rows = await _db.CashTransactions + .AsNoTracking() + .Where(t => t.ShiftId == shiftId && t.CafeId == cafeId) + .OrderBy(t => t.CreatedAt) + .ToListAsync(cancellationToken); + + return rows.Select(ToTransactionDto).ToList(); + } + + public async Task> RecordTransactionAsync( + string cafeId, + string shiftId, + CashTransactionType type, + PaymentMethod method, + decimal amount, + string createdByUserId, + string? referenceId = null, + string? note = null, + CancellationToken cancellationToken = default) + { + if (amount <= 0) + return new ShiftServiceResult(false, null, "INVALID_AMOUNT", "amount"); + + var shift = await _db.RegisterShifts.FirstOrDefaultAsync( + s => s.Id == shiftId && s.CafeId == cafeId, + cancellationToken); + + if (shift is null) + return new ShiftServiceResult(false, null, "SHIFT_NOT_FOUND"); + + if (shift.Status != ShiftStatus.Open) + return new ShiftServiceResult(false, null, "SHIFT_ALREADY_CLOSED"); + + var tx = new CashTransaction + { + CafeId = cafeId, + BranchId = shift.BranchId, + ShiftId = shiftId, + Type = type, + Method = method, + Amount = amount, + ReferenceId = referenceId, + Note = note, + CreatedByUserId = createdByUserId, + CreatedAt = DateTime.UtcNow + }; + + _db.CashTransactions.Add(tx); + await _db.SaveChangesAsync(cancellationToken); + + return new ShiftServiceResult(true, ToTransactionDto(tx)); + } + + public async Task> RequireOpenShiftForBranchAsync( + string cafeId, + string branchId, + CancellationToken cancellationToken = default) + { + var shift = await _db.RegisterShifts.FirstOrDefaultAsync( + s => s.CafeId == cafeId && s.BranchId == branchId && s.Status == ShiftStatus.Open, + cancellationToken); + + if (shift is null) + return new ShiftServiceResult(false, null, "NO_OPEN_SHIFT", "branchId"); + + return new ShiftServiceResult(true, shift); + } + + internal static decimal CalculateExpectedCash(decimal openingCash, IEnumerable transactions) + { + var cashPayments = transactions + .Where(t => t.Type == CashTransactionType.OrderPayment && t.Method == PaymentMethod.Cash) + .Sum(t => t.Amount); + + var withdrawals = transactions + .Where(t => t.Type == CashTransactionType.Withdrawal) + .Sum(t => t.Amount); + + return openingCash + cashPayments - withdrawals; + } + + private static ShiftDto ToDto(Shift s) => new( + s.Id, + s.CafeId, + s.BranchId, + s.OpenedByUserId, + s.ClosedByUserId, + s.OpenedAt, + s.ClosedAt, + s.OpeningCash, + s.ClosingCash, + s.ExpectedCash, + s.Discrepancy, + s.Status); + + private static CashTransactionDto ToTransactionDto(CashTransaction t) => new( + t.Id, + t.ShiftId, + t.BranchId, + t.Type, + t.Method, + t.Amount, + t.ReferenceId, + t.Note, + t.CreatedByUserId, + t.CreatedAt); +} diff --git a/src/Meezi.API/Services/SmsMarketingService.cs b/src/Meezi.API/Services/SmsMarketingService.cs new file mode 100644 index 0000000..3088c92 --- /dev/null +++ b/src/Meezi.API/Services/SmsMarketingService.cs @@ -0,0 +1,131 @@ +using Meezi.API.Models.Crm; +using Meezi.Core.Constants; +using Meezi.Core.Enums; +using Meezi.Core.Interfaces; +using Meezi.Core.Utilities; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using StackExchange.Redis; + +namespace Meezi.API.Services; + +public interface ISmsMarketingService +{ + Task GetUsageAsync(string cafeId, PlanTier planTier, CancellationToken cancellationToken = default); + Task<(bool Success, SmsCampaignResult? Data, string? ErrorCode, string? Message)> SendCampaignAsync( + string cafeId, + PlanTier planTier, + SendSmsCampaignRequest request, + CancellationToken cancellationToken = default); +} + +public class SmsMarketingService : ISmsMarketingService +{ + private readonly AppDbContext _db; + private readonly ISmsService _smsService; + private readonly IConnectionMultiplexer _redis; + private readonly ILogger _logger; + + public SmsMarketingService( + AppDbContext db, + ISmsService smsService, + IConnectionMultiplexer redis, + ILogger logger) + { + _db = db; + _smsService = smsService; + _redis = redis; + _logger = logger; + } + + public async Task GetUsageAsync( + string cafeId, + PlanTier planTier, + CancellationToken cancellationToken = default) + { + var month = DateTime.UtcNow.ToString("yyyy-MM"); + var used = await GetUsedCountAsync(cafeId, month); + var limit = PlanLimits.MaxSmsPerMonth(planTier); + return new SmsUsageDto(used, limit == int.MaxValue ? -1 : limit, month); + } + + public async Task<(bool Success, SmsCampaignResult? Data, string? ErrorCode, string? Message)> SendCampaignAsync( + string cafeId, + PlanTier planTier, + SendSmsCampaignRequest request, + CancellationToken cancellationToken = default) + { + var maxSms = PlanLimits.MaxSmsPerMonth(planTier); + if (maxSms == 0) + return (false, null, "PLAN_LIMIT_REACHED", "SMS is not available on the Free plan."); + + var phones = await ResolvePhonesAsync(cafeId, request, cancellationToken); + if (phones.Count == 0) + return (false, null, "NOT_FOUND", "No recipients found."); + + var month = DateTime.UtcNow.ToString("yyyy-MM"); + var used = await GetUsedCountAsync(cafeId, month); + if (maxSms != int.MaxValue && used + phones.Count > maxSms) + return (false, null, "PLAN_LIMIT_REACHED", "Monthly SMS limit would be exceeded."); + + var sent = 0; + var failed = 0; + + foreach (var phone in phones) + { + try + { + await _smsService.SendMessageAsync(phone, request.Message, cancellationToken); + sent++; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to send SMS to recipient"); + failed++; + } + } + + if (sent > 0) + await IncrementUsageAsync(cafeId, month, sent); + + return (true, new SmsCampaignResult(sent, failed), null, null); + } + + private async Task> ResolvePhonesAsync( + string cafeId, + SendSmsCampaignRequest request, + CancellationToken cancellationToken) + { + if (request.Phones is { Count: > 0 }) + { + return request.Phones + .Select(PhoneNormalizer.Normalize) + .Where(PhoneNormalizer.IsValidIranMobile) + .Distinct() + .ToList(); + } + + var query = _db.Customers.Where(c => c.CafeId == cafeId); + if (request.TargetGroup.HasValue) + query = query.Where(c => c.Group == request.TargetGroup.Value); + + return await query.Select(c => c.Phone).Distinct().ToListAsync(cancellationToken); + } + + private async Task GetUsedCountAsync(string cafeId, string month) + { + var redis = _redis.GetDatabase(); + var value = await redis.StringGetAsync(UsageKey(cafeId, month)); + return value.HasValue ? (int)value : 0; + } + + private async Task IncrementUsageAsync(string cafeId, string month, int count) + { + var redis = _redis.GetDatabase(); + var key = UsageKey(cafeId, month); + await redis.StringIncrementAsync(key, count); + await redis.KeyExpireAsync(key, TimeSpan.FromDays(40)); + } + + private static string UsageKey(string cafeId, string month) => $"sms:usage:{cafeId}:{month}"; +} diff --git a/src/Meezi.API/Services/SnappfoodWebhookService.cs b/src/Meezi.API/Services/SnappfoodWebhookService.cs new file mode 100644 index 0000000..297976f --- /dev/null +++ b/src/Meezi.API/Services/SnappfoodWebhookService.cs @@ -0,0 +1,187 @@ +using System.Security.Cryptography; +using System.Text; +using Meezi.API.Models.Orders; +using Meezi.API.Services.Printing; +using Meezi.API.Models.Snappfood; +using Meezi.Core.Entities; +using Meezi.Core.Enums; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; + +namespace Meezi.API.Services; + +public interface ISnappfoodWebhookService +{ + bool VerifySignature(string rawBody, string? signatureHeader); + Task<(bool Success, string? Error)> ProcessOrderAsync(SnappfoodWebhookOrder order, CancellationToken cancellationToken = default); +} + +public class SnappfoodWebhookService : ISnappfoodWebhookService +{ + private readonly AppDbContext _db; + private readonly IKdsNotifier _kdsNotifier; + private readonly IConfiguration _configuration; + private readonly ILogger _logger; + + public SnappfoodWebhookService( + AppDbContext db, + IKdsNotifier kdsNotifier, + IConfiguration configuration, + ILogger logger) + { + _db = db; + _kdsNotifier = kdsNotifier; + _configuration = configuration; + _logger = logger; + } + + public bool VerifySignature(string rawBody, string? signatureHeader) + { + var secret = _configuration["Snappfood:WebhookSecret"]; + if (string.IsNullOrWhiteSpace(secret)) + return true; + + if (string.IsNullOrWhiteSpace(signatureHeader)) + return false; + + var expected = ComputeHmac(rawBody, secret); + return CryptographicOperations.FixedTimeEquals( + Encoding.UTF8.GetBytes(expected), + Encoding.UTF8.GetBytes(signatureHeader.Trim())); + } + + public async Task<(bool Success, string? Error)> ProcessOrderAsync( + SnappfoodWebhookOrder order, + CancellationToken cancellationToken = default) + { + var cafe = await _db.Cafes + .FirstOrDefaultAsync(c => c.SnappfoodVendorId == order.VendorId, cancellationToken); + + if (cafe is null) + return (false, "Unknown vendor."); + + var existing = await _db.Orders + .AnyAsync(o => o.CafeId == cafe.Id && o.SnappfoodOrderId == order.OrderId, cancellationToken); + if (existing) + return (true, null); + + var menuItems = await _db.MenuItems + .Where(m => m.CafeId == cafe.Id && m.IsAvailable) + .ToListAsync(cancellationToken); + + var orderItems = new List(); + decimal subtotal = 0; + + foreach (var item in order.Items) + { + var menuItem = menuItems.FirstOrDefault(m => + m.Name.Equals(item.Name, StringComparison.OrdinalIgnoreCase) || + (m.NameEn != null && m.NameEn.Equals(item.Name, StringComparison.OrdinalIgnoreCase))); + + if (menuItem is null) + { + _logger.LogWarning("Snappfood item {Name} not matched for cafe {CafeId}", item.Name, cafe.Id); + continue; + } + + var lineTotal = item.UnitPrice * item.Quantity; + subtotal += lineTotal; + orderItems.Add(new OrderItem + { + MenuItemId = menuItem.Id, + Quantity = item.Quantity, + UnitPrice = item.UnitPrice, + Notes = "Snappfood" + }); + } + + if (orderItems.Count == 0) + return (false, "No menu items matched."); + + var taxRate = await _db.Taxes + .Where(t => t.CafeId == cafe.Id && t.IsDefault) + .Select(t => t.Rate) + .FirstOrDefaultAsync(cancellationToken); + + var taxTotal = Math.Round(subtotal * taxRate / 100m, 0); + var total = order.Total > 0 ? order.Total : subtotal + taxTotal; + + var displayNumber = await AllocateDisplayNumberAsync(cafe.Id, cancellationToken); + var meeziOrder = new Order + { + CafeId = cafe.Id, + OrderType = OrderType.Delivery, + Status = OrderStatus.Confirmed, + DisplayNumber = displayNumber, + SnappfoodOrderId = order.OrderId, + Subtotal = subtotal, + TaxTotal = taxTotal, + Total = total, + Items = orderItems + }; + + if (!string.IsNullOrWhiteSpace(order.CustomerPhone)) + { + var customer = await _db.Customers + .FirstOrDefaultAsync(c => c.CafeId == cafe.Id && c.Phone == order.CustomerPhone, cancellationToken); + if (customer is null) + { + customer = new Customer + { + CafeId = cafe.Id, + Name = order.CustomerName ?? "Snappfood", + Phone = order.CustomerPhone, + Group = CustomerGroup.New + }; + _db.Customers.Add(customer); + await _db.SaveChangesAsync(cancellationToken); + } + + meeziOrder.CustomerId = customer.Id; + } + + _db.Orders.Add(meeziOrder); + await _db.SaveChangesAsync(cancellationToken); + + var loaded = await _db.Orders + .Include(o => o.Items) + .ThenInclude(i => i.MenuItem) + .Include(o => o.Table) + .FirstAsync(o => o.Id == meeziOrder.Id, cancellationToken); + + await _kdsNotifier.NotifyOrderCreatedAsync(cafe.Id, MapLive(loaded), cancellationToken); + return (true, null); + } + + private static string ComputeHmac(string body, string secret) + { + using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret)); + var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(body)); + return Convert.ToHexString(hash).ToLowerInvariant(); + } + + + private async Task AllocateDisplayNumberAsync(string cafeId, CancellationToken ct) + { + var max = await _db.Orders + .Where(o => o.CafeId == cafeId) + .MaxAsync(o => (int?)o.DisplayNumber, ct); + return (max ?? 0) + 1; + } private static LiveOrderDto MapLive(Order o) => new( + o.Id, + o.DisplayNumber > 0 ? o.DisplayNumber : ReceiptPrintFormatting.StableDisplayNumberFromId(o.Id), + o.Status, + o.Table?.Number, + o.OrderType, + o.Total, + o.CreatedAt, + o.Items.Select(i => new OrderItemDto( + i.Id, + i.MenuItemId, + i.MenuItem?.Name ?? "", + i.Quantity, + i.UnitPrice, + i.Notes, + i.IsVoided, + i.VoidedAt)).ToList()); +} diff --git a/src/Meezi.API/Services/TableService.cs b/src/Meezi.API/Services/TableService.cs new file mode 100644 index 0000000..b5c8df7 --- /dev/null +++ b/src/Meezi.API/Services/TableService.cs @@ -0,0 +1,1255 @@ +using Meezi.API.Models.Tables; + +using Meezi.Core.Entities; + +using Meezi.Core.Enums; + +using Meezi.Infrastructure.Data; + +using Microsoft.EntityFrameworkCore; + +using QRCoder; + + + +namespace Meezi.API.Services; + + + +public interface ITableService + +{ + + Task> GetTableBoardAsync( + + string cafeId, + + bool activeOnly = true, + + string? branchId = null, + + CancellationToken cancellationToken = default); + + + + Task> GetTablesAsync( + + string cafeId, + + string? branchId = null, + + CancellationToken cancellationToken = default); + + + + Task CreateTableAsync(string cafeId, CreateTableRequest request, CancellationToken cancellationToken = default); + + Task PatchTableAsync(string cafeId, string tableId, PatchTableRequest request, CancellationToken cancellationToken = default); + + Task SetTableCleaningAsync( + + string cafeId, + + string tableId, + + bool isCleaning, + + CancellationToken cancellationToken = default); + + Task> DeleteTableAsync( + string cafeId, + string tableId, + CancellationToken cancellationToken = default); + + Task> GetBranchTableBoardAsync( + + string cafeId, + + string branchId, + + bool activeOnly = true, + + CancellationToken cancellationToken = default); + + + + Task?> GetBranchTablesAsync( + string cafeId, + string branchId, + CancellationToken cancellationToken = default); + + + + Task> CreateBranchTableAsync( + + string cafeId, + + string branchId, + + CreateBranchTableRequest request, + + CancellationToken cancellationToken = default); + + + + Task> PatchBranchTableAsync( + + string cafeId, + + string branchId, + + string tableId, + + PatchBranchTableRequest request, + + CancellationToken cancellationToken = default); + + + + Task> DeleteBranchTableAsync( + + string cafeId, + + string branchId, + + string tableId, + + CancellationToken cancellationToken = default); + + + + Task?> GetBranchSectionsAsync( + + string cafeId, + + string branchId, + + CancellationToken cancellationToken = default); + + + + Task> CreateBranchSectionAsync( + + string cafeId, + + string branchId, + + CreateTableSectionRequest request, + + CancellationToken cancellationToken = default); + + + + Task> PatchBranchSectionAsync( + + string cafeId, + + string branchId, + + string sectionId, + + PatchTableSectionRequest request, + + CancellationToken cancellationToken = default); + + + + Task> DeleteBranchSectionAsync( + + string cafeId, + + string branchId, + + string sectionId, + + CancellationToken cancellationToken = default); + + + + Task CanAccessBranchAsync( + + string cafeId, + + string branchId, + + string? userId, + + EmployeeRole? role, + + CancellationToken cancellationToken = default); + + + + Task ResolveQrAsync(string qrCode, CancellationToken cancellationToken = default); + + Task GetQrPngAsync(string cafeId, string tableId, CancellationToken cancellationToken = default); + +} + + + +public class TableService : ITableService + +{ + + private static readonly OrderStatus[] OpenOrderStatuses = + + [ + + OrderStatus.Pending, + + OrderStatus.Confirmed, + + OrderStatus.Preparing, + + OrderStatus.Ready + + ]; + + + + private readonly AppDbContext _db; + + private readonly IConfiguration _configuration; + + private readonly IKdsNotifier _kdsNotifier; + + private readonly IBranchIdentityService _identity; + + + + public TableService( + AppDbContext db, + IConfiguration configuration, + IKdsNotifier kdsNotifier, + IBranchIdentityService identity) + + { + + _db = db; + + _configuration = configuration; + + _kdsNotifier = kdsNotifier; + + _identity = identity; + + } + + + + public async Task> GetTableBoardAsync( + + string cafeId, + + bool activeOnly = true, + + string? branchId = null, + + CancellationToken cancellationToken = default) + + { + + var query = _db.Tables.Where(t => t.CafeId == cafeId); + + if (!string.IsNullOrEmpty(branchId)) + + query = query.Where(t => t.BranchId == branchId); + + if (activeOnly) + + query = query.Where(t => t.IsActive); + + + + var tables = await LoadTablesOrderedAsync(query, cancellationToken); + + return await BuildBoardDtosAsync(cafeId, tables, cancellationToken); + + } + + + + public Task> GetBranchTableBoardAsync( + + string cafeId, + + string branchId, + + bool activeOnly = true, + + CancellationToken cancellationToken = default) => + + GetTableBoardAsync(cafeId, activeOnly, branchId, cancellationToken); + + + + public async Task> GetTablesAsync( + + string cafeId, + + string? branchId = null, + + CancellationToken cancellationToken = default) + + { + + var query = _db.Tables.Where(t => t.CafeId == cafeId && t.IsActive); + + if (!string.IsNullOrEmpty(branchId)) + + query = query.Where(t => t.BranchId == branchId); + + var tables = await LoadTablesOrderedAsync(query, cancellationToken); + + return tables.Select(ToDto).ToList(); + + } + + + + public async Task?> GetBranchTablesAsync( + string cafeId, + string branchId, + CancellationToken cancellationToken = default) + { + if (!await BranchExistsAsync(cafeId, branchId, cancellationToken)) + return null; + + + + return await GetTablesAsync(cafeId, branchId, cancellationToken); + + } + + + + public async Task CreateTableAsync( + + string cafeId, + + CreateTableRequest request, + + CancellationToken cancellationToken = default) + + { + + var branchId = request.BranchId; + + if (string.IsNullOrEmpty(branchId)) + + branchId = await GetDefaultBranchIdAsync(cafeId, cancellationToken); + + if (branchId is null) return null; + + + + if (!await BranchExistsAsync(cafeId, branchId, cancellationToken)) + + return null; + + + + var entity = new Table + + { + + CafeId = cafeId, + + BranchId = branchId, + + Number = request.Number.Trim(), + + Capacity = request.Capacity, + + Floor = request.Floor?.Trim(), + + QrCode = Guid.NewGuid().ToString("N"), + + IsActive = request.IsActive + + }; + + + + _db.Tables.Add(entity); + + await _db.SaveChangesAsync(cancellationToken); + + return ToDto(entity); + + } + + + + public async Task> CreateBranchTableAsync( + + string cafeId, + + string branchId, + + CreateBranchTableRequest request, + + CancellationToken cancellationToken = default) + + { + + if (!await BranchExistsAsync(cafeId, branchId, cancellationToken)) + + return Fail("BRANCH_NOT_FOUND", "Branch not found."); + + + + if (!string.IsNullOrEmpty(request.SectionId)) + + { + + var sectionOk = await _db.TableSections.AnyAsync( + + s => s.Id == request.SectionId && s.CafeId == cafeId && s.BranchId == branchId, + + cancellationToken); + + if (!sectionOk) + + return Fail("SECTION_NOT_FOUND", "Section not found."); + + } + + + + var entity = new Table + + { + + CafeId = cafeId, + + BranchId = branchId, + + SectionId = request.SectionId, + + Number = request.Number.Trim(), + + Capacity = request.Capacity, + + Floor = request.Floor?.Trim(), + + SortOrder = request.SortOrder, + + QrCode = Guid.NewGuid().ToString("N"), + + IsActive = request.IsActive + + }; + + + + _db.Tables.Add(entity); + + await _db.SaveChangesAsync(cancellationToken); + + await _db.Entry(entity).Reference(t => t.Section).LoadAsync(cancellationToken); + + return Ok(ToDto(entity)); + + } + + + + public async Task PatchTableAsync( + + string cafeId, + + string tableId, + + PatchTableRequest request, + + CancellationToken cancellationToken = default) + + { + + var entity = await _db.Tables + + .Include(t => t.Section) + + .FirstOrDefaultAsync(t => t.Id == tableId && t.CafeId == cafeId, cancellationToken); + + if (entity is null) return null; + + + + if (request.Number is not null) entity.Number = request.Number.Trim(); + + if (request.Capacity.HasValue) entity.Capacity = request.Capacity.Value; + + if (request.Floor is not null) entity.Floor = request.Floor.Trim(); + + if (request.BranchId is not null && !string.IsNullOrEmpty(request.BranchId)) + + { + + if (!await BranchExistsAsync(cafeId, request.BranchId, cancellationToken)) + + return null; + + entity.BranchId = request.BranchId; + + } + + if (request.ImageUrl is not null) + + entity.ImageUrl = string.IsNullOrWhiteSpace(request.ImageUrl) ? null : request.ImageUrl; + + if (request.VideoUrl is not null) + + entity.VideoUrl = string.IsNullOrWhiteSpace(request.VideoUrl) ? null : request.VideoUrl; + + if (request.IsActive.HasValue) entity.IsActive = request.IsActive.Value; + + if (request.IsCleaning.HasValue) entity.IsCleaning = request.IsCleaning.Value; + + + + await _db.SaveChangesAsync(cancellationToken); + + return ToDto(entity); + + } + + + + public async Task> PatchBranchTableAsync( + + string cafeId, + + string branchId, + + string tableId, + + PatchBranchTableRequest request, + + CancellationToken cancellationToken = default) + + { + + var entity = await _db.Tables + + .Include(t => t.Section) + + .FirstOrDefaultAsync( + + t => t.Id == tableId && t.CafeId == cafeId && t.BranchId == branchId, + + cancellationToken); + + if (entity is null) + + return Fail("TABLE_NOT_FOUND", "Table not found."); + + + + if (request.SectionId is not null) + + { + + if (string.IsNullOrEmpty(request.SectionId)) + + entity.SectionId = null; + + else + + { + + var sectionOk = await _db.TableSections.AnyAsync( + + s => s.Id == request.SectionId && s.CafeId == cafeId && s.BranchId == branchId, + + cancellationToken); + + if (!sectionOk) + + return Fail("SECTION_NOT_FOUND", "Section not found."); + + entity.SectionId = request.SectionId; + + } + + } + + + + if (request.Number is not null) entity.Number = request.Number.Trim(); + + if (request.Capacity.HasValue) entity.Capacity = request.Capacity.Value; + + if (request.Floor is not null) entity.Floor = request.Floor.Trim(); + + if (request.SortOrder.HasValue) entity.SortOrder = request.SortOrder.Value; + + if (request.ImageUrl is not null) + + entity.ImageUrl = string.IsNullOrWhiteSpace(request.ImageUrl) ? null : request.ImageUrl; + + if (request.VideoUrl is not null) + + entity.VideoUrl = string.IsNullOrWhiteSpace(request.VideoUrl) ? null : request.VideoUrl; + + if (request.IsActive.HasValue) entity.IsActive = request.IsActive.Value; + + if (request.IsCleaning.HasValue) entity.IsCleaning = request.IsCleaning.Value; + + + + await _db.SaveChangesAsync(cancellationToken); + + await _db.Entry(entity).Reference(t => t.Section).LoadAsync(cancellationToken); + + return Ok(ToDto(entity)); + + } + + + + public async Task> DeleteTableAsync( + string cafeId, + string tableId, + CancellationToken cancellationToken = default) + { + var branchId = await _db.Tables + .Where(t => t.Id == tableId && t.CafeId == cafeId) + .Select(t => t.BranchId) + .FirstOrDefaultAsync(cancellationToken); + if (string.IsNullOrEmpty(branchId)) + return Fail("TABLE_NOT_FOUND", "Table not found."); + return await DeleteBranchTableAsync(cafeId, branchId, tableId, cancellationToken); + } + + public async Task> DeleteBranchTableAsync( + + string cafeId, + + string branchId, + + string tableId, + + CancellationToken cancellationToken = default) + + { + + var entity = await _db.Tables.FirstOrDefaultAsync( + + t => t.Id == tableId && t.CafeId == cafeId && t.BranchId == branchId, + + cancellationToken); + + if (entity is null) + + return Fail("TABLE_NOT_FOUND", "Table not found."); + + + + var hasOpenOrder = await _db.Orders.AnyAsync( + + o => o.CafeId == cafeId + + && o.TableId == tableId + + && OpenOrderStatuses.Contains(o.Status), + + cancellationToken); + + if (hasOpenOrder) + + return Fail("TABLE_HAS_OPEN_ORDER", "This table has an open order."); + + + + entity.DeletedAt = DateTime.UtcNow; + + entity.IsActive = false; + + entity.IsCleaning = false; + + await _db.SaveChangesAsync(cancellationToken); + + await _kdsNotifier.NotifyTableStatusChangedAsync(cafeId, cancellationToken); + + return Ok(new { tableId }); + + } + + + + public async Task?> GetBranchSectionsAsync( + + string cafeId, + + string branchId, + + CancellationToken cancellationToken = default) + + { + + if (!await BranchExistsAsync(cafeId, branchId, cancellationToken)) + + return null; + + + + var sections = await _db.TableSections + .Where(s => s.CafeId == cafeId && s.BranchId == branchId) + .OrderBy(s => s.SortOrder) + .ThenBy(s => s.Name) + .ToListAsync(cancellationToken); + + var tableCounts = await _db.Tables + .Where(t => t.CafeId == cafeId && t.BranchId == branchId && t.SectionId != null) + .GroupBy(t => t.SectionId!) + .Select(g => new { SectionId = g.Key, Count = g.Count() }) + .ToDictionaryAsync(x => x.SectionId, x => x.Count, cancellationToken); + + return sections + .Select(s => new TableSectionDto( + s.Id, + s.BranchId, + s.Name, + s.SortOrder, + s.IsActive, + tableCounts.GetValueOrDefault(s.Id))) + .ToList(); + + } + + + + public async Task> CreateBranchSectionAsync( + + string cafeId, + + string branchId, + + CreateTableSectionRequest request, + + CancellationToken cancellationToken = default) + + { + + if (!await BranchExistsAsync(cafeId, branchId, cancellationToken)) + + return Fail("BRANCH_NOT_FOUND", "Branch not found."); + + + + var entity = new TableSection + + { + + CafeId = cafeId, + + BranchId = branchId, + + Name = request.Name.Trim(), + + SortOrder = request.SortOrder + + }; + + _db.TableSections.Add(entity); + + await _db.SaveChangesAsync(cancellationToken); + + return Ok(new TableSectionDto(entity.Id, entity.BranchId, entity.Name, entity.SortOrder, entity.IsActive, 0)); + + } + + + + public async Task> PatchBranchSectionAsync( + + string cafeId, + + string branchId, + + string sectionId, + + PatchTableSectionRequest request, + + CancellationToken cancellationToken = default) + + { + + var entity = await _db.TableSections.FirstOrDefaultAsync( + + s => s.Id == sectionId && s.CafeId == cafeId && s.BranchId == branchId, + + cancellationToken); + + if (entity is null) + + return Fail("SECTION_NOT_FOUND", "Section not found."); + + + + if (request.Name is not null) entity.Name = request.Name.Trim(); + + if (request.SortOrder.HasValue) entity.SortOrder = request.SortOrder.Value; + + if (request.IsActive.HasValue) entity.IsActive = request.IsActive.Value; + + + + await _db.SaveChangesAsync(cancellationToken); + + var tableCount = await _db.Tables.CountAsync( + + t => t.SectionId == sectionId && t.CafeId == cafeId, + + cancellationToken); + + return Ok(new TableSectionDto(entity.Id, entity.BranchId, entity.Name, entity.SortOrder, entity.IsActive, tableCount)); + + } + + + + public async Task> DeleteBranchSectionAsync( + + string cafeId, + + string branchId, + + string sectionId, + + CancellationToken cancellationToken = default) + + { + + var entity = await _db.TableSections.FirstOrDefaultAsync( + + s => s.Id == sectionId && s.CafeId == cafeId && s.BranchId == branchId, + + cancellationToken); + + if (entity is null) + + return Fail("SECTION_NOT_FOUND", "Section not found."); + + + + var hasTables = await _db.Tables.AnyAsync( + + t => t.SectionId == sectionId && t.CafeId == cafeId && t.BranchId == branchId, + + cancellationToken); + + if (hasTables) + + return Fail("TABLE_SECTION_HAS_TABLES", "Section has tables assigned."); + + + + entity.DeletedAt = DateTime.UtcNow; + + await _db.SaveChangesAsync(cancellationToken); + + return Ok(new { sectionId }); + + } + + + + public async Task CanAccessBranchAsync( + + string cafeId, + + string branchId, + + string? userId, + + EmployeeRole? role, + + CancellationToken cancellationToken = default) + + { + + if (!await BranchExistsAsync(cafeId, branchId, cancellationToken)) + + return false; + + if (role is not EmployeeRole.Manager) + + return true; + + if (string.IsNullOrEmpty(userId)) + + return false; + + var employee = await _db.Employees.FirstOrDefaultAsync( + + e => e.Id == userId && e.CafeId == cafeId, + + cancellationToken); + + return employee?.BranchId == branchId; + + } + + + + public async Task SetTableCleaningAsync( + + string cafeId, + + string tableId, + + bool isCleaning, + + CancellationToken cancellationToken = default) + + { + + var entity = await _db.Tables.FirstOrDefaultAsync(t => t.Id == tableId && t.CafeId == cafeId, cancellationToken); + + if (entity is null) return null; + + + + entity.IsCleaning = isCleaning; + + await _db.SaveChangesAsync(cancellationToken); + + await _kdsNotifier.NotifyTableStatusChangedAsync(cafeId, cancellationToken); + + var board = await GetTableBoardAsync(cafeId, activeOnly: false, branchId: entity.BranchId, cancellationToken); + + return board.FirstOrDefault(t => t.Id == tableId); + + } + + + + public async Task ResolveQrAsync(string qrCode, CancellationToken cancellationToken = default) + + { + + var table = await _db.Tables + .Include(t => t.Cafe) + .Include(t => t.Branch) + .FirstOrDefaultAsync( + t => t.QrCode == qrCode && t.IsActive && t.DeletedAt == null, + cancellationToken); + + if (table?.Cafe is null || table.Branch is null || !table.Branch.IsActive) + return null; + + var identity = await _identity.GetEffectiveIdentityAsync( + table.CafeId, + table.BranchId, + cancellationToken); + + return new QrResolveResponse( + table.CafeId, + table.Cafe.Slug, + table.Id, + table.Number, + table.Number, + table.BranchId, + table.Branch.Name, + table.Cafe.Name, + identity?.PrimaryColor ?? "#0F6E56", + identity?.LogoUrl ?? table.Cafe.LogoUrl, + identity?.WelcomeText ?? "خوش آمدید", + identity?.WifiPassword, + identity?.Address, + table.IsCleaning); + + } + + + + public async Task GetQrPngAsync(string cafeId, string tableId, CancellationToken cancellationToken = default) + + { + + var table = await _db.Tables + + .FirstOrDefaultAsync(t => t.Id == tableId && t.CafeId == cafeId, cancellationToken); + + if (table is null) return null; + + + + var url = BuildPublicQrUrl(table.QrCode); + + using var generator = new QRCodeGenerator(); + + using var data = generator.CreateQrCode(url, QRCodeGenerator.ECCLevel.Q); + + var png = new PngByteQRCode(data); + + return png.GetGraphic(20); + + } + + + + private static BranchTableOperationResult Ok(T data) => + + new(true, data, null, null); + + + + private static BranchTableOperationResult Fail(string code, string message) => + + new(false, default, code, message); + + + + private async Task BranchExistsAsync(string cafeId, string branchId, CancellationToken ct) => + + await _db.Branches.AnyAsync(b => b.Id == branchId && b.CafeId == cafeId, ct); + + + + private async Task GetDefaultBranchIdAsync(string cafeId, CancellationToken ct) => + + await _db.Branches + + .Where(b => b.CafeId == cafeId && b.IsActive) + + .OrderBy(b => b.CreatedAt) + + .Select(b => b.Id) + + .FirstOrDefaultAsync(ct); + + + + private static async Task> LoadTablesOrderedAsync( + + IQueryable query, + + CancellationToken ct) + + { + + var tables = await query.Include(t => t.Section).ToListAsync(ct); + + return tables + + .OrderBy(t => t.Section?.SortOrder ?? int.MaxValue) + + .ThenBy(t => t.SortOrder) + + .ThenBy(t => t.Number, StringComparer.Ordinal) + + .ToList(); + + } + + + + private async Task> BuildBoardDtosAsync( + + string cafeId, + + List
tables, + + CancellationToken cancellationToken) + + { + + if (tables.Count == 0) + + return []; + + + + var tableIds = tables.Select(t => t.Id).ToList(); + + var today = DateOnly.FromDateTime(DateTime.UtcNow); + + + + var openOrders = await _db.Orders + + .Include(o => o.Customer) + + .Where(o => o.CafeId == cafeId + + && o.TableId != null + + && tableIds.Contains(o.TableId) + + && OpenOrderStatuses.Contains(o.Status)) + + .ToListAsync(cancellationToken); + + + + var reservations = await _db.TableReservations + + .Where(r => r.CafeId == cafeId + + && r.TableId != null + + && tableIds.Contains(r.TableId!) + + && r.Date == today + + && r.Status != ReservationStatus.Cancelled) + + .ToListAsync(cancellationToken); + + + + return tables.Select(t => + + { + + var order = openOrders.FirstOrDefault(o => o.TableId == t.Id); + + var reservation = reservations.FirstOrDefault(r => r.TableId == t.Id && order is null); + + + + var status = t.IsCleaning + + ? TableBoardStatus.Cleaning + + : order is not null + + ? TableBoardStatus.Busy + + : reservation is not null + + ? TableBoardStatus.Reserved + + : TableBoardStatus.Free; + + + + TableCurrentOrderSummary? current = null; + + if (order is not null) + + { + + var guestLabel = !string.IsNullOrWhiteSpace(order.GuestName) + + ? order.GuestName + + : order.Customer?.Name; + + current = new TableCurrentOrderSummary( + order.Id, + order.Status, + order.Total, + guestLabel, + order.Source); + + } + + else if (reservation is not null) + + { + + current = new TableCurrentOrderSummary( + + reservation.Id, + + OrderStatus.Pending, + + 0, + + reservation.GuestName); + + } + + + + return new TableBoardDto( + + t.Id, + + t.BranchId, + + t.SectionId, + + t.Section?.Name, + + t.SortOrder, + + t.Number, + + t.Capacity, + + t.Floor, + + t.QrCode, + + BuildPublicQrUrl(t.QrCode), + + t.ImageUrl, + + t.VideoUrl, + + t.IsActive, + + status, + + current, + + t.IsCleaning); + + }).ToList(); + + } + + + + private TableDto ToDto(Table t) => new( + + t.Id, + + t.BranchId, + + t.SectionId, + + t.Section?.Name, + + t.SortOrder, + + t.Number, + + t.Capacity, + + t.Floor, + + t.QrCode, + + BuildPublicQrUrl(t.QrCode), + + t.ImageUrl, + + t.VideoUrl, + + t.IsActive); + + + + private string BuildPublicQrUrl(string qrCode) + + { + + var baseUrl = _configuration["App:QrPublicBaseUrl"]?.TrimEnd('/') + + ?? "https://meezi.ir"; + + return $"{baseUrl}/q/{qrCode}"; + + } + +} + + diff --git a/src/Meezi.API/Services/TaxService.cs b/src/Meezi.API/Services/TaxService.cs new file mode 100644 index 0000000..3f9d23a --- /dev/null +++ b/src/Meezi.API/Services/TaxService.cs @@ -0,0 +1,111 @@ +using Meezi.API.Models.Taxes; +using Meezi.Core.Entities; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; + +namespace Meezi.API.Services; + +public interface ITaxService +{ + Task> GetAllAsync(string cafeId, CancellationToken cancellationToken = default); + Task CreateAsync(string cafeId, CreateTaxRequest request, CancellationToken cancellationToken = default); + Task UpdateAsync(string cafeId, string id, UpdateTaxRequest request, CancellationToken cancellationToken = default); + Task DeleteAsync(string cafeId, string id, CancellationToken cancellationToken = default); +} + +public class TaxService : ITaxService +{ + private readonly AppDbContext _db; + + public TaxService(AppDbContext db) => _db = db; + + public async Task> GetAllAsync(string cafeId, CancellationToken cancellationToken = default) + { + return await _db.Taxes + .Where(t => t.CafeId == cafeId) + .OrderBy(t => t.Name) + .Select(t => ToDto(t)) + .ToListAsync(cancellationToken); + } + + public async Task CreateAsync(string cafeId, CreateTaxRequest request, CancellationToken cancellationToken = default) + { + if (request.IsDefault) + { + var existing = await _db.Taxes.Where(t => t.CafeId == cafeId && t.IsDefault).ToListAsync(cancellationToken); + foreach (var t in existing) t.IsDefault = false; + } + + var entity = new Tax + { + CafeId = cafeId, + Name = request.Name, + Rate = request.Rate, + IsDefault = request.IsDefault, + IsRequired = request.IsRequired, + IsCompound = request.IsCompound + }; + _db.Taxes.Add(entity); + await _db.SaveChangesAsync(cancellationToken); + return ToDto(entity); + } + + public async Task UpdateAsync( + string cafeId, + string id, + UpdateTaxRequest request, + CancellationToken cancellationToken = default) + { + var entity = await _db.Taxes.FirstOrDefaultAsync(t => t.Id == id && t.CafeId == cafeId, cancellationToken); + if (entity is null) return null; + + if (request.Name is not null) entity.Name = request.Name; + if (request.Rate.HasValue) entity.Rate = request.Rate.Value; + if (request.IsDefault == true) + { + var others = await _db.Taxes.Where(t => t.CafeId == cafeId && t.Id != id).ToListAsync(cancellationToken); + foreach (var t in others) t.IsDefault = false; + entity.IsDefault = true; + } + else if (request.IsDefault.HasValue) entity.IsDefault = request.IsDefault.Value; + + if (request.IsRequired.HasValue) entity.IsRequired = request.IsRequired.Value; + if (request.IsCompound.HasValue) entity.IsCompound = request.IsCompound.Value; + + await _db.SaveChangesAsync(cancellationToken); + return ToDto(entity); + } + + public async Task DeleteAsync(string cafeId, string id, CancellationToken cancellationToken = default) + { + var entity = await _db.Taxes.FirstOrDefaultAsync(t => t.Id == id && t.CafeId == cafeId, cancellationToken); + if (entity is null) return false; + + var wasDefault = entity.IsDefault; + entity.DeletedAt = DateTime.UtcNow; + entity.IsDefault = false; + + var categories = await _db.MenuCategories + .Where(c => c.CafeId == cafeId && c.TaxId == id) + .ToListAsync(cancellationToken); + + Tax? replacementDefault = null; + if (wasDefault) + { + replacementDefault = await _db.Taxes + .Where(t => t.CafeId == cafeId && t.Id != id) + .OrderBy(t => t.Name) + .FirstOrDefaultAsync(cancellationToken); + if (replacementDefault is not null) + replacementDefault.IsDefault = true; + } + + foreach (var category in categories) + category.TaxId = replacementDefault?.Id; + + await _db.SaveChangesAsync(cancellationToken); + return true; + } + + private static TaxDto ToDto(Tax t) => new(t.Id, t.Name, t.Rate, t.IsDefault, t.IsRequired, t.IsCompound); +} diff --git a/src/Meezi.API/Services/TerminalRegistryService.cs b/src/Meezi.API/Services/TerminalRegistryService.cs new file mode 100644 index 0000000..bf40781 --- /dev/null +++ b/src/Meezi.API/Services/TerminalRegistryService.cs @@ -0,0 +1,90 @@ +using Meezi.Core.Constants; +using Meezi.Core.Enums; +using StackExchange.Redis; + +namespace Meezi.API.Services; + +public record TerminalInfoDto(string TerminalId, DateTime? LastSeenUtc); + +public interface ITerminalRegistryService +{ + Task<(bool Allowed, string? ErrorCode, string? Message)> RegisterAsync( + string cafeId, + PlanTier tier, + string terminalId, + CancellationToken cancellationToken = default); + + Task> ListAsync(string cafeId, CancellationToken cancellationToken = default); + + Task RevokeAsync(string cafeId, string terminalId, CancellationToken cancellationToken = default); +} + +public class TerminalRegistryService : ITerminalRegistryService +{ + private static readonly TimeSpan TerminalTtl = TimeSpan.FromDays(90); + private readonly IConnectionMultiplexer _redis; + + public TerminalRegistryService(IConnectionMultiplexer redis) => _redis = redis; + + public async Task<(bool Allowed, string? ErrorCode, string? Message)> RegisterAsync( + string cafeId, + PlanTier tier, + string terminalId, + CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(terminalId)) + return (false, "TERMINAL_ID_REQUIRED", "Terminal id is required."); + + terminalId = terminalId.Trim(); + var db = _redis.GetDatabase(); + var setKey = $"terminals:{cafeId}"; + var max = PlanLimits.MaxTerminals(tier); + + if (max == int.MaxValue) + { + await db.SetAddAsync(setKey, terminalId); + await db.KeyExpireAsync(setKey, TerminalTtl); + return (true, null, null); + } + + var members = await db.SetMembersAsync(setKey); + var known = members.Select(m => m.ToString()).ToHashSet(StringComparer.Ordinal); + if (known.Contains(terminalId)) + { + await db.KeyExpireAsync(setKey, TerminalTtl); + return (true, null, null); + } + + if (known.Count >= max) + return (false, "PLAN_LIMIT_REACHED", "Terminal limit reached for your plan. Please upgrade."); + + await db.SetAddAsync(setKey, terminalId); + await db.KeyExpireAsync(setKey, TerminalTtl); + return (true, null, null); + } + + public async Task> ListAsync( + string cafeId, + CancellationToken cancellationToken = default) + { + var db = _redis.GetDatabase(); + var setKey = $"terminals:{cafeId}"; + var members = await db.SetMembersAsync(setKey); + return members + .Select(m => m.ToString()) + .Where(id => !string.IsNullOrEmpty(id)) + .Select(id => new TerminalInfoDto(id!, null)) + .OrderBy(t => t.TerminalId) + .ToList(); + } + + public async Task RevokeAsync( + string cafeId, + string terminalId, + CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(terminalId)) return; + var db = _redis.GetDatabase(); + await db.SetRemoveAsync($"terminals:{cafeId}", terminalId.Trim()); + } +} diff --git a/src/Meezi.API/Utils/JalaliCalendarHelper.cs b/src/Meezi.API/Utils/JalaliCalendarHelper.cs new file mode 100644 index 0000000..63304fd --- /dev/null +++ b/src/Meezi.API/Utils/JalaliCalendarHelper.cs @@ -0,0 +1,104 @@ +using System.Globalization; + +namespace Meezi.API.Utils; + +public static class JalaliCalendarHelper +{ + private static readonly PersianCalendar Persian = new(); + + public static TimeZoneInfo IranTimeZone => + TimeZoneInfo.TryFindSystemTimeZoneById("Asia/Tehran", out var tz) + ? tz + : TimeZoneInfo.FindSystemTimeZoneById("Iran Standard Time"); + + public static bool TryParseJalaliDate(string? input, out int year, out int month, out int day) + { + year = month = day = 0; + if (string.IsNullOrWhiteSpace(input)) + return false; + + var normalized = input.Trim().Replace('/', '-'); + var parts = normalized.Split('-', StringSplitOptions.RemoveEmptyEntries); + if (parts.Length != 3) + return false; + + if (!int.TryParse(parts[0], out year) || !int.TryParse(parts[1], out month) || !int.TryParse(parts[2], out day)) + return false; + + if (year is < 1300 or > 1500 || month is < 1 or > 12 || day is < 1 or > 31) + return false; + + try + { + _ = Persian.ToDateTime(year, month, day, 0, 0, 0, 0); + return true; + } + catch + { + return false; + } + } + + public static bool TryParseJalaliMonth(string? input, out int year, out int month) + { + year = month = 0; + if (string.IsNullOrWhiteSpace(input)) + return false; + + var normalized = input.Trim().Replace('/', '-'); + var parts = normalized.Split('-', StringSplitOptions.RemoveEmptyEntries); + if (parts.Length != 2) + return false; + + if (!int.TryParse(parts[0], out year) || !int.TryParse(parts[1], out month)) + return false; + + if (year is < 1300 or > 1500 || month is < 1 or > 12) + return false; + + try + { + _ = Persian.GetDaysInMonth(year, month); + return true; + } + catch + { + return false; + } + } + + public static string FormatJalaliDate(int year, int month, int day) => + $"{year}-{month:D2}-{day:D2}"; + + public static string FormatJalaliMonth(int year, int month) => + $"{year}-{month:D2}"; + + public static (int Year, int Month, int Day) TodayJalali() + { + var local = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, IranTimeZone); + return (Persian.GetYear(local), Persian.GetMonth(local), Persian.GetDayOfMonth(local)); + } + + public static (DateTime UtcStart, DateTime UtcEnd) GetUtcRangeForJalaliDay(int year, int month, int day) + { + var localStart = Persian.ToDateTime(year, month, day, 0, 0, 0, 0); + var localEndExclusive = localStart.AddDays(1); + var utcStart = TimeZoneInfo.ConvertTimeToUtc(DateTime.SpecifyKind(localStart, DateTimeKind.Unspecified), IranTimeZone); + var utcEnd = TimeZoneInfo.ConvertTimeToUtc(DateTime.SpecifyKind(localEndExclusive, DateTimeKind.Unspecified), IranTimeZone); + return (utcStart, utcEnd); + } + + public static (DateTime UtcStart, DateTime UtcEnd) GetUtcRangeForJalaliMonth(int year, int month) + { + var daysInMonth = Persian.GetDaysInMonth(year, month); + var (utcStart, _) = GetUtcRangeForJalaliDay(year, month, 1); + var (_, utcEnd) = GetUtcRangeForJalaliDay(year, month, daysInMonth); + return (utcStart, utcEnd); + } + + public static (int Year, int Month, int Day) ToJalali(DateTime utc) + { + var local = TimeZoneInfo.ConvertTimeFromUtc(utc, IranTimeZone); + return (Persian.GetYear(local), Persian.GetMonth(local), Persian.GetDayOfMonth(local)); + } +} diff --git a/src/Meezi.API/Validators/AuthValidators.cs b/src/Meezi.API/Validators/AuthValidators.cs new file mode 100644 index 0000000..f2f673a --- /dev/null +++ b/src/Meezi.API/Validators/AuthValidators.cs @@ -0,0 +1,39 @@ +using FluentValidation; +using Meezi.API.Models.Auth; +using Meezi.Core.Utilities; + +namespace Meezi.API.Validators; + +public class SendOtpRequestValidator : AbstractValidator +{ + public SendOtpRequestValidator() + { + RuleFor(x => x.Phone) + .NotEmpty() + .Must(p => PhoneNormalizer.IsValidIranMobile(PhoneNormalizer.Normalize(p))) + .WithMessage("Invalid Iranian mobile number."); + } +} + +public class VerifyOtpRequestValidator : AbstractValidator +{ + public VerifyOtpRequestValidator() + { + RuleFor(x => x.Phone) + .NotEmpty() + .Must(p => PhoneNormalizer.IsValidIranMobile(PhoneNormalizer.Normalize(p))) + .WithMessage("Invalid Iranian mobile number."); + + RuleFor(x => x.Code) + .Must(OtpNormalizer.IsValidSixDigitCode) + .WithMessage("OTP must be 6 digits."); + } +} + +public class RefreshTokenRequestValidator : AbstractValidator +{ + public RefreshTokenRequestValidator() + { + RuleFor(x => x.RefreshToken).NotEmpty(); + } +} diff --git a/src/Meezi.API/Validators/BillingValidators.cs b/src/Meezi.API/Validators/BillingValidators.cs new file mode 100644 index 0000000..6a8d676 --- /dev/null +++ b/src/Meezi.API/Validators/BillingValidators.cs @@ -0,0 +1,17 @@ +using FluentValidation; +using Meezi.API.Models.Billing; +using Meezi.Core.Constants; +using Meezi.Core.Enums; + +namespace Meezi.API.Validators; + +public class SubscribeRequestValidator : AbstractValidator +{ + public SubscribeRequestValidator() + { + RuleFor(x => x.PlanTier) + .Must(PlanPricing.IsBillableOnline) + .WithMessage("This plan must be purchased via sales contact."); + RuleFor(x => x.Months).InclusiveBetween(1, 12); + } +} diff --git a/src/Meezi.API/Validators/BranchMenuValidators.cs b/src/Meezi.API/Validators/BranchMenuValidators.cs new file mode 100644 index 0000000..fe1582a --- /dev/null +++ b/src/Meezi.API/Validators/BranchMenuValidators.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Meezi.API.Models.Menu; + +namespace Meezi.API.Validators; + +public class UpsertBranchMenuOverrideRequestValidator : AbstractValidator +{ + public UpsertBranchMenuOverrideRequestValidator() + { + RuleFor(x => x.PriceOverride) + .GreaterThanOrEqualTo(0) + .When(x => x.PriceOverride.HasValue); + } +} diff --git a/src/Meezi.API/Validators/CoffeeAdvisorValidators.cs b/src/Meezi.API/Validators/CoffeeAdvisorValidators.cs new file mode 100644 index 0000000..4e4aa90 --- /dev/null +++ b/src/Meezi.API/Validators/CoffeeAdvisorValidators.cs @@ -0,0 +1,13 @@ +using FluentValidation; +using Meezi.API.Models.Public; + +namespace Meezi.API.Validators; + +public class CoffeeAdvisorRequestValidator : AbstractValidator +{ + public CoffeeAdvisorRequestValidator() + { + RuleFor(x => x.Purpose).NotEmpty().MinimumLength(3).MaximumLength(500); + RuleFor(x => x.CafeSlug).MaximumLength(80).When(x => !string.IsNullOrEmpty(x.CafeSlug)); + } +} diff --git a/src/Meezi.API/Validators/CreateBranchRequestValidator.cs b/src/Meezi.API/Validators/CreateBranchRequestValidator.cs new file mode 100644 index 0000000..969385c --- /dev/null +++ b/src/Meezi.API/Validators/CreateBranchRequestValidator.cs @@ -0,0 +1,32 @@ +using FluentValidation; +using Meezi.API.Models.Branches; +using Meezi.Core.Utilities; + +namespace Meezi.API.Validators; + +public class CreateBranchRequestValidator : AbstractValidator +{ + public CreateBranchRequestValidator() + { + RuleFor(x => x.Name).NotEmpty().MaximumLength(200); + RuleFor(x => x.LoginPhone) + .NotEmpty() + .Must(p => PhoneNormalizer.IsValidIranMobile(PhoneNormalizer.Normalize(p))) + .WithMessage("Invalid Iranian mobile number for branch login."); + RuleFor(x => x.ManagerName).MaximumLength(200).When(x => x.ManagerName is not null); + RuleFor(x => x.Address).MaximumLength(500).When(x => x.Address is not null); + RuleFor(x => x.City).MaximumLength(100).When(x => x.City is not null); + RuleFor(x => x.Phone).MaximumLength(20).When(x => x.Phone is not null); + } +} + +public class PatchBranchRequestValidator : AbstractValidator +{ + public PatchBranchRequestValidator() + { + RuleFor(x => x.Name).NotEmpty().MaximumLength(200).When(x => x.Name is not null); + RuleFor(x => x.Address).MaximumLength(500).When(x => x.Address is not null); + RuleFor(x => x.City).MaximumLength(100).When(x => x.City is not null); + RuleFor(x => x.Phone).MaximumLength(20).When(x => x.Phone is not null); + } +} diff --git a/src/Meezi.API/Validators/CrmValidators.cs b/src/Meezi.API/Validators/CrmValidators.cs new file mode 100644 index 0000000..194c150 --- /dev/null +++ b/src/Meezi.API/Validators/CrmValidators.cs @@ -0,0 +1,68 @@ +using FluentValidation; +using Meezi.API.Models.Crm; +using Meezi.Core.Utilities; + +namespace Meezi.API.Validators; + +public class CreateCustomerRequestValidator : AbstractValidator +{ + public CreateCustomerRequestValidator() + { + RuleFor(x => x.Name).NotEmpty().MaximumLength(200); + RuleFor(x => x.Phone) + .NotEmpty() + .Must(p => PhoneNormalizer.IsValidIranMobile(PhoneNormalizer.Normalize(p))) + .WithMessage("Invalid Iranian mobile number."); + RuleFor(x => x.NationalId) + .Matches(@"^\d{10}$") + .When(x => !string.IsNullOrWhiteSpace(x.NationalId)) + .WithMessage("National ID must be 10 digits."); + RuleFor(x => x.Group).IsInEnum(); + } +} + +public class UpdateCustomerRequestValidator : AbstractValidator +{ + public UpdateCustomerRequestValidator() + { + RuleFor(x => x.Name) + .NotEmpty() + .MaximumLength(200) + .When(x => x.Name is not null); + RuleFor(x => x.Phone) + .Must(p => PhoneNormalizer.IsValidIranMobile(PhoneNormalizer.Normalize(p!))) + .When(x => !string.IsNullOrWhiteSpace(x.Phone)) + .WithMessage("Invalid Iranian mobile number."); + RuleFor(x => x.NationalId) + .Matches(@"^\d{10}$") + .When(x => !string.IsNullOrWhiteSpace(x.NationalId)) + .WithMessage("National ID must be 10 digits."); + RuleFor(x => x.Group) + .IsInEnum() + .When(x => x.Group.HasValue); + RuleFor(x => x.LoyaltyPoints) + .GreaterThanOrEqualTo(0) + .When(x => x.LoyaltyPoints.HasValue); + } +} + +public class CreateCouponRequestValidator : AbstractValidator +{ + public CreateCouponRequestValidator() + { + RuleFor(x => x.Code).NotEmpty().MaximumLength(50); + RuleFor(x => x.Type).IsInEnum(); + RuleFor(x => x.Value).GreaterThan(0); + } +} + +public class SendSmsCampaignRequestValidator : AbstractValidator +{ + public SendSmsCampaignRequestValidator() + { + RuleFor(x => x.Message).NotEmpty().MaximumLength(500); + RuleFor(x => x) + .Must(x => x.TargetGroup.HasValue || (x.Phones is { Count: > 0 })) + .WithMessage("Specify TargetGroup or at least one phone number."); + } +} diff --git a/src/Meezi.API/Validators/ExpenseValidators.cs b/src/Meezi.API/Validators/ExpenseValidators.cs new file mode 100644 index 0000000..b5fa91d --- /dev/null +++ b/src/Meezi.API/Validators/ExpenseValidators.cs @@ -0,0 +1,15 @@ +using FluentValidation; +using Meezi.API.Models.Expenses; + +namespace Meezi.API.Validators; + +public class CreateExpenseRequestValidator : AbstractValidator +{ + public CreateExpenseRequestValidator() + { + RuleFor(x => x.BranchId).NotEmpty(); + RuleFor(x => x.Amount).GreaterThan(0); + RuleFor(x => x.Note).MaximumLength(500).When(x => x.Note is not null); + RuleFor(x => x.ReceiptImageUrl).MaximumLength(500).When(x => x.ReceiptImageUrl is not null); + } +} diff --git a/src/Meezi.API/Validators/HrValidators.cs b/src/Meezi.API/Validators/HrValidators.cs new file mode 100644 index 0000000..7953485 --- /dev/null +++ b/src/Meezi.API/Validators/HrValidators.cs @@ -0,0 +1,33 @@ +using FluentValidation; +using Meezi.API.Models.Hr; +using Meezi.Core.Enums; + +namespace Meezi.API.Validators; + +public class CreateLeaveRequestValidator : AbstractValidator +{ + public CreateLeaveRequestValidator() + { + RuleFor(x => x.EndDate).GreaterThanOrEqualTo(x => x.StartDate); + } +} + +public class ReviewLeaveRequestValidator : AbstractValidator +{ + public ReviewLeaveRequestValidator() + { + RuleFor(x => x.Status) + .Must(s => s is LeaveStatus.Approved or LeaveStatus.Rejected) + .WithMessage("Status must be Approved or Rejected."); + } +} + +public class CreateSalaryRequestValidator : AbstractValidator +{ + public CreateSalaryRequestValidator() + { + RuleFor(x => x.EmployeeId).NotEmpty(); + RuleFor(x => x.MonthYear).Matches(@"^\d{4}-\d{2}$"); + RuleFor(x => x.BaseSalary).GreaterThanOrEqualTo(0); + } +} diff --git a/src/Meezi.API/Validators/KitchenStationValidators.cs b/src/Meezi.API/Validators/KitchenStationValidators.cs new file mode 100644 index 0000000..b9811c4 --- /dev/null +++ b/src/Meezi.API/Validators/KitchenStationValidators.cs @@ -0,0 +1,22 @@ +using FluentValidation; +using Meezi.API.Models.Kitchen; + +namespace Meezi.API.Validators; + +public class CreateKitchenStationRequestValidator : AbstractValidator +{ + public CreateKitchenStationRequestValidator() + { + RuleFor(x => x.Name).NotEmpty().MaximumLength(100); + RuleFor(x => x.PrinterPort).InclusiveBetween(1, 65535); + } +} + +public class UpdateKitchenStationRequestValidator : AbstractValidator +{ + public UpdateKitchenStationRequestValidator() + { + RuleFor(x => x.Name).MaximumLength(100).When(x => x.Name is not null); + RuleFor(x => x.PrinterPort).InclusiveBetween(1, 65535).When(x => x.PrinterPort.HasValue); + } +} diff --git a/src/Meezi.API/Validators/PatchCafeSettingsRequestValidator.cs b/src/Meezi.API/Validators/PatchCafeSettingsRequestValidator.cs new file mode 100644 index 0000000..f0204f1 --- /dev/null +++ b/src/Meezi.API/Validators/PatchCafeSettingsRequestValidator.cs @@ -0,0 +1,28 @@ +using FluentValidation; +using Meezi.API.Models.Cafes; + +namespace Meezi.API.Validators; + +public class PatchCafeSettingsRequestValidator : AbstractValidator +{ + public PatchCafeSettingsRequestValidator() + { + RuleFor(x => x.Name).MaximumLength(200).When(x => x.Name is not null); + RuleFor(x => x.Phone).MaximumLength(20).When(x => x.Phone is not null); + RuleFor(x => x.Address).MaximumLength(500).When(x => x.Address is not null); + RuleFor(x => x.City).MaximumLength(100).When(x => x.City is not null); + RuleFor(x => x.Description).MaximumLength(2000).When(x => x.Description is not null); + RuleFor(x => x.LogoUrl).MaximumLength(500).When(x => x.LogoUrl is not null); + RuleFor(x => x.CoverImageUrl).MaximumLength(500).When(x => x.CoverImageUrl is not null); + RuleFor(x => x.SnappfoodVendorId).MaximumLength(100).When(x => x.SnappfoodVendorId is not null); + When(x => x.Theme is not null, () => + { + RuleFor(x => x.Theme!.PaletteId).NotEmpty().MaximumLength(48); + RuleFor(x => x.Theme!.PanelStyle).NotEmpty().MaximumLength(24); + RuleFor(x => x.Theme!.MenuStyle).NotEmpty().MaximumLength(24); + RuleFor(x => x.Theme!.MenuTexture).NotEmpty().MaximumLength(24); + RuleFor(x => x.Theme!.Density).NotEmpty().MaximumLength(24); + RuleFor(x => x.Theme!.Radius).NotEmpty().MaximumLength(24); + }); + } +} diff --git a/src/Meezi.API/Validators/PosValidators.cs b/src/Meezi.API/Validators/PosValidators.cs new file mode 100644 index 0000000..04948be --- /dev/null +++ b/src/Meezi.API/Validators/PosValidators.cs @@ -0,0 +1,169 @@ +using FluentValidation; +using Meezi.API.Models.Menu; +using Meezi.API.Models.Orders; +using Meezi.API.Models.Tables; +using Meezi.Core.Constants; +using Meezi.Core.Utilities; + +namespace Meezi.API.Validators; + +public class CreateMenuCategoryRequestValidator : AbstractValidator +{ + public CreateMenuCategoryRequestValidator() + { + RuleFor(x => x.Name).NotEmpty().MaximumLength(200); + RuleFor(x => x.NameEn).NotEmpty().MaximumLength(200); + RuleFor(x => x.SortOrder).GreaterThanOrEqualTo(0); + RuleFor(x => x.DiscountPercent).InclusiveBetween(0, 100); + RuleFor(x => x.Icon).MaximumLength(32).When(x => x.Icon is not null); + RuleFor(x => x.IconPresetId) + .Must(id => id is null || CategoryIconPresets.IsValidPreset(id)) + .WithMessage("Invalid icon preset.") + .MaximumLength(48) + .When(x => x.IconPresetId is not null); + RuleFor(x => x.IconStyle) + .Must(s => s is null || CategoryIconPresets.IsValidStyle(s)) + .WithMessage("Invalid icon style.") + .MaximumLength(16) + .When(x => x.IconStyle is not null); + RuleFor(x => x.ImageUrl).MaximumLength(500).When(x => x.ImageUrl is not null); + RuleFor(x => x) + .Must(x => string.IsNullOrWhiteSpace(x.IconPresetId) == string.IsNullOrWhiteSpace(x.IconStyle)) + .WithMessage("Icon preset and style must be set together."); + } +} + +public class CreateMenuItemRequestValidator : AbstractValidator +{ + public CreateMenuItemRequestValidator() + { + RuleFor(x => x.CategoryId).NotEmpty(); + RuleFor(x => x.Name).NotEmpty().MaximumLength(200); + RuleFor(x => x.NameEn).NotEmpty().MaximumLength(200); + RuleFor(x => x.Price).GreaterThanOrEqualTo(0); + RuleFor(x => x.DiscountPercent).InclusiveBetween(0, 100); + } +} + +public class CreateTableRequestValidator : AbstractValidator +{ + public CreateTableRequestValidator() + { + RuleFor(x => x.Number).NotEmpty().MaximumLength(50); + RuleFor(x => x.Capacity).GreaterThan(0); + } +} + +public class PatchTableRequestValidator : AbstractValidator +{ + public PatchTableRequestValidator() + { + RuleFor(x => x.Number).NotEmpty().MaximumLength(50).When(x => x.Number is not null); + RuleFor(x => x.Capacity).GreaterThan(0).When(x => x.Capacity.HasValue); + } +} + +public class CreateBranchTableRequestValidator : AbstractValidator +{ + public CreateBranchTableRequestValidator() + { + RuleFor(x => x.Number).NotEmpty().MaximumLength(50); + RuleFor(x => x.Capacity).GreaterThan(0); + } +} + +public class PatchBranchTableRequestValidator : AbstractValidator +{ + public PatchBranchTableRequestValidator() + { + RuleFor(x => x.Number).NotEmpty().MaximumLength(50).When(x => x.Number is not null); + RuleFor(x => x.Capacity).GreaterThan(0).When(x => x.Capacity.HasValue); + } +} + +public class CreateTableSectionRequestValidator : AbstractValidator +{ + public CreateTableSectionRequestValidator() + { + RuleFor(x => x.Name).NotEmpty().MaximumLength(100); + } +} + +public class PatchTableSectionRequestValidator : AbstractValidator +{ + public PatchTableSectionRequestValidator() + { + RuleFor(x => x.Name).NotEmpty().MaximumLength(100).When(x => x.Name is not null); + } +} + +public class CreateOrderRequestValidator : AbstractValidator +{ + public CreateOrderRequestValidator() + { + RuleFor(x => x.OrderType).IsInEnum(); + RuleFor(x => x.Items).NotEmpty(); + RuleFor(x => x.GuestName).MaximumLength(200).When(x => x.GuestName is not null); + RuleFor(x => x.GuestPhone) + .Must(p => PhoneNormalizer.IsValidIranMobile(PhoneNormalizer.Normalize(p!))) + .When(x => !string.IsNullOrWhiteSpace(x.GuestPhone)); + RuleForEach(x => x.Items).ChildRules(item => + { + item.RuleFor(i => i.MenuItemId).NotEmpty(); + item.RuleFor(i => i.Quantity).GreaterThan(0); + }); + } +} + +public class UpdateOrderStatusRequestValidator : AbstractValidator +{ + public UpdateOrderStatusRequestValidator() + { + RuleFor(x => x.Status).IsInEnum(); + } +} + +public class RecordPaymentsRequestValidator : AbstractValidator +{ + public RecordPaymentsRequestValidator() + { + RuleFor(x => x.Payments).NotEmpty(); + RuleForEach(x => x.Payments).ChildRules(p => + { + p.RuleFor(x => x.Method).IsInEnum(); + p.RuleFor(x => x.Amount).GreaterThan(0); + }); + RuleFor(x => x.LoyaltyPointsToRedeem) + .GreaterThanOrEqualTo(0) + .When(x => x.LoyaltyPointsToRedeem.HasValue); + } +} + +public class AppendOrderItemsRequestValidator : AbstractValidator +{ + public AppendOrderItemsRequestValidator() + { + RuleFor(x => x.Items).NotEmpty(); + RuleForEach(x => x.Items).ChildRules(item => + { + item.RuleFor(i => i.MenuItemId).NotEmpty(); + item.RuleFor(i => i.Quantity).GreaterThan(0); + }); + } +} + +public class UpdateOrderSessionRequestValidator : AbstractValidator +{ + public UpdateOrderSessionRequestValidator() + { + RuleFor(x => x.GuestName).MaximumLength(200).When(x => x.GuestName is not null); + RuleFor(x => x.GuestPhone) + .Must(p => PhoneNormalizer.IsValidIranMobile(PhoneNormalizer.Normalize(p!))) + .When(x => !string.IsNullOrWhiteSpace(x.GuestPhone)); + } +} + +public class SetTableCleaningRequestValidator : AbstractValidator +{ + public SetTableCleaningRequestValidator() { } +} diff --git a/src/Meezi.API/Validators/PrintValidators.cs b/src/Meezi.API/Validators/PrintValidators.cs new file mode 100644 index 0000000..606530b --- /dev/null +++ b/src/Meezi.API/Validators/PrintValidators.cs @@ -0,0 +1,44 @@ +using FluentValidation; +using Meezi.API.Models.Printing; + +namespace Meezi.API.Validators; + +public class PatchBranchPrintSettingsRequestValidator : AbstractValidator +{ + public PatchBranchPrintSettingsRequestValidator() + { + RuleFor(x => x.ReceiptPrinterPort) + .InclusiveBetween(1, 65535) + .When(x => x.ReceiptPrinterPort.HasValue); + RuleFor(x => x.KitchenPrinterPort) + .InclusiveBetween(1, 65535) + .When(x => x.KitchenPrinterPort.HasValue); + RuleFor(x => x.PaperWidthMm) + .Must(w => w is null or 58 or 80) + .WithMessage("Paper width must be 58 or 80 mm."); + RuleFor(x => x.ReceiptHeader).MaximumLength(500).When(x => x.ReceiptHeader is not null); + RuleFor(x => x.ReceiptFooter).MaximumLength(500).When(x => x.ReceiptFooter is not null); + RuleFor(x => x.WifiPassword).MaximumLength(100).When(x => x.WifiPassword is not null); + RuleFor(x => x.PosDevicePort) + .InclusiveBetween(1, 65535) + .When(x => x.PosDevicePort.HasValue); + } +} + +public class PosPaymentRequestValidator : AbstractValidator +{ + public PosPaymentRequestValidator() + { + RuleFor(x => x.OrderId).NotEmpty().MaximumLength(64); + RuleFor(x => x.Amount).GreaterThan(0); + } +} + +public class TestPrintRequestValidator : AbstractValidator +{ + public TestPrintRequestValidator() + { + RuleFor(x => x.PrinterIp).NotEmpty().MaximumLength(45); + RuleFor(x => x.Port).InclusiveBetween(1, 65535); + } +} diff --git a/src/Meezi.API/Validators/PublicValidators.cs b/src/Meezi.API/Validators/PublicValidators.cs new file mode 100644 index 0000000..0c1a56e --- /dev/null +++ b/src/Meezi.API/Validators/PublicValidators.cs @@ -0,0 +1,42 @@ +using FluentValidation; +using Meezi.API.Models.Public; + +namespace Meezi.API.Validators; + +public class GuestCreateOrderRequestValidator : AbstractValidator +{ + public GuestCreateOrderRequestValidator() + { + RuleFor(x => x.Items).NotEmpty(); + RuleForEach(x => x.Items).ChildRules(item => + { + item.RuleFor(i => i.MenuItemId).NotEmpty(); + item.RuleFor(i => i.Quantity).GreaterThan(0); + }); + } +} + +public class PlaceGuestOrderRequestValidator : AbstractValidator +{ + public PlaceGuestOrderRequestValidator() + { + RuleFor(x => x.TableId).NotEmpty(); + RuleFor(x => x.Items).NotEmpty(); + RuleForEach(x => x.Items).ChildRules(item => + { + item.RuleFor(i => i.MenuItemId).NotEmpty(); + item.RuleFor(i => i.Quantity).GreaterThan(0); + }); + } +} + +public class CreateReservationRequestValidator : AbstractValidator +{ + public CreateReservationRequestValidator() + { + RuleFor(x => x.GuestName).NotEmpty().MaximumLength(200); + RuleFor(x => x.GuestPhone).NotEmpty().Matches(@"^09\d{9}$"); + RuleFor(x => x.PartySize).InclusiveBetween(1, 20); + RuleFor(x => x.Date).Must(d => d >= DateOnly.FromDateTime(DateTime.UtcNow.Date)); + } +} diff --git a/src/Meezi.API/Validators/ReviewValidators.cs b/src/Meezi.API/Validators/ReviewValidators.cs new file mode 100644 index 0000000..7df8587 --- /dev/null +++ b/src/Meezi.API/Validators/ReviewValidators.cs @@ -0,0 +1,23 @@ +using FluentValidation; +using Meezi.API.Models.Public; + +namespace Meezi.API.Validators; + +public class CreateCafeReviewRequestValidator : AbstractValidator +{ + public CreateCafeReviewRequestValidator() + { + RuleFor(x => x.AuthorName).NotEmpty().MaximumLength(200); + RuleFor(x => x.Rating).InclusiveBetween(1, 5); + RuleFor(x => x.AuthorPhone).Matches(@"^09\d{9}$").When(x => !string.IsNullOrEmpty(x.AuthorPhone)); + RuleFor(x => x.Comment).MaximumLength(2000); + } +} + +public class ReplyCafeReviewRequestValidator : AbstractValidator +{ + public ReplyCafeReviewRequestValidator() + { + RuleFor(x => x.Reply).NotEmpty().MaximumLength(2000); + } +} diff --git a/src/Meezi.API/Validators/ShiftValidators.cs b/src/Meezi.API/Validators/ShiftValidators.cs new file mode 100644 index 0000000..8b0da93 --- /dev/null +++ b/src/Meezi.API/Validators/ShiftValidators.cs @@ -0,0 +1,20 @@ +using FluentValidation; +using Meezi.API.Models.Shifts; + +namespace Meezi.API.Validators; + +public class OpenShiftRequestValidator : AbstractValidator +{ + public OpenShiftRequestValidator() + { + RuleFor(x => x.OpeningCash).GreaterThanOrEqualTo(0); + } +} + +public class CloseShiftRequestValidator : AbstractValidator +{ + public CloseShiftRequestValidator() + { + RuleFor(x => x.ClosingCash).GreaterThanOrEqualTo(0); + } +} diff --git a/src/Meezi.API/appsettings.Development.json b/src/Meezi.API/appsettings.Development.json new file mode 100644 index 0000000..248fb94 --- /dev/null +++ b/src/Meezi.API/appsettings.Development.json @@ -0,0 +1,21 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Host=localhost;Port=5434;Database=meezi;Username=meezi;Password=meezi_local_pass", + "Redis": "localhost:6381" + }, + "Auth": { + "MaxOtpAttemptsPerHour": 100 + }, + "Kavenegar": { + "ApiKey": "" + }, + "Ai3d": { + "AllowDevStub": true + }, + "Logging": { + "LogLevel": { + "Default": "Debug", + "Microsoft.AspNetCore": "Information" + } + } +} diff --git a/src/Meezi.API/appsettings.json b/src/Meezi.API/appsettings.json new file mode 100644 index 0000000..1b7bf96 --- /dev/null +++ b/src/Meezi.API/appsettings.json @@ -0,0 +1,108 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Host=localhost;Port=5434;Database=meezi;Username=meezi;Password=meezi_local_pass", + "Redis": "localhost:6381" + }, + "Jwt": { + "Key": "meezi-dev-secret-key-min-32-chars!!", + "Issuer": "meezi", + "Audience": "meezi", + "AccessTokenExpiryDays": 7, + "RefreshTokenExpiryDays": 30 + }, + "App": { + "PublicBaseUrl": "https://localhost:7208", + "QrPublicBaseUrl": "https://meezi.ir" + }, + "Auth": { + "MaxOtpAttemptsPerHour": 5 + }, + "Security": { + "Enabled": true, + "RequireCaptchaOnPublicWrites": false, + "Turnstile": { + "SiteKey": "", + "SecretKey": "" + }, + "RateLimits": { + "AuthOtpPerIpPerHour": 15, + "PublicReadsPerIpPerMinute": 120, + "PublicWritesPerIpPerMinute": 30 + }, + "GuestOrders": { + "PerIpPerCafePerHour": 25, + "PerCafePerHour": 200, + "PerIpGlobalPerHour": 60 + } + }, + "Kavenegar": { + "ApiKey": "", + "OtpTemplate": "verify" + }, + "ZarinPal": { + "MerchantId": "", + "Sandbox": true + }, + "Billing": { + "DashboardBaseUrl": "http://localhost:3101" + }, + "Snappfood": { + "WebhookSecret": "meezi-dev-snappfood-secret", + "ApiKey": "", + "ApiBaseUrl": "" + }, + "DeliveryPlatforms": { + "DefaultSnappfoodCommissionPercent": 18, + "DefaultTap30CommissionPercent": 15, + "DefaultDigikalaCommissionPercent": 12, + "Snappfood": { + "WebhookSecret": "meezi-dev-snappfood-secret", + "ApiKey": "", + "ApiBaseUrl": "" + }, + "Tap30": { + "WebhookSecret": "meezi-dev-tap30-secret", + "ApiKey": "", + "ApiBaseUrl": "" + }, + "Digikala": { + "WebhookSecret": "", + "Enabled": false + } + }, + "Taraz": { + "Username": "" + }, + "OpenAI": { + "ApiKey": "", + "Model": "gpt-4o-mini" + }, + "Cors": { + "Origins": [ "http://localhost:3000", "http://localhost:3100", "http://localhost:3101" ] + }, + "Serilog": { + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft": "Warning", + "Microsoft.EntityFrameworkCore": "Warning", + "Hangfire": "Information" + } + } + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "Ai3d": { + "ApiKey": "", + "BaseUrl": "https://api.meshy.ai", + "ImageTo3dPath": "/openapi/v1/image-to-3d", + "PollIntervalSeconds": 4, + "PollTimeoutSeconds": 300, + "AllowDevStub": false + } +} diff --git a/src/Meezi.Admin.API/Controllers/AdminApiControllerBase.cs b/src/Meezi.Admin.API/Controllers/AdminApiControllerBase.cs new file mode 100644 index 0000000..f926163 --- /dev/null +++ b/src/Meezi.Admin.API/Controllers/AdminApiControllerBase.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Meezi.Core.Interfaces; +using Meezi.Shared; + +namespace Meezi.Admin.API.Controllers; + +[Authorize(Roles = "SystemAdmin")] +[ApiController] +public abstract class AdminApiControllerBase : ControllerBase +{ + protected string RequireAdminId(ITenantContext tenant) + { + if (!tenant.IsSystemAdmin || string.IsNullOrEmpty(tenant.UserId)) + throw new InvalidOperationException("System admin context required."); + return tenant.UserId; + } +} diff --git a/src/Meezi.Admin.API/Controllers/AdminAuthController.cs b/src/Meezi.Admin.API/Controllers/AdminAuthController.cs new file mode 100644 index 0000000..e07ab89 --- /dev/null +++ b/src/Meezi.Admin.API/Controllers/AdminAuthController.cs @@ -0,0 +1,85 @@ +using FluentValidation; +using Microsoft.AspNetCore.Mvc; +using Meezi.Admin.API.Models; +using Meezi.Admin.API.Services; +using Meezi.Shared; + +namespace Meezi.Admin.API.Controllers; + +[ApiController] +[Route("api/admin/auth")] +public class AdminAuthController : ControllerBase +{ + private readonly IAdminAuthService _auth; + private readonly IValidator _sendOtpValidator; + private readonly IValidator _verifyOtpValidator; + private readonly IValidator _refreshValidator; + + public AdminAuthController( + IAdminAuthService auth, + IValidator sendOtpValidator, + IValidator verifyOtpValidator, + IValidator refreshValidator) + { + _auth = auth; + _sendOtpValidator = sendOtpValidator; + _verifyOtpValidator = verifyOtpValidator; + _refreshValidator = refreshValidator; + } + + [HttpPost("send-otp")] + public async Task SendOtp([FromBody] SendOtpRequest request, CancellationToken cancellationToken) + { + var validation = await _sendOtpValidator.ValidateAsync(request, cancellationToken); + if (!validation.IsValid) + return BadRequest(ValidationError(validation)); + + var (success, data, code, message) = await _auth.SendOtpAsync(request, cancellationToken); + if (!success) + return ErrorResult(code!, message!); + + return Ok(new ApiResponse(true, data)); + } + + [HttpPost("verify-otp")] + public async Task VerifyOtp([FromBody] VerifyOtpRequest request, CancellationToken cancellationToken) + { + var validation = await _verifyOtpValidator.ValidateAsync(request, cancellationToken); + if (!validation.IsValid) + return BadRequest(ValidationError(validation)); + + var (success, data, code, message) = await _auth.VerifyOtpAsync(request, cancellationToken); + if (!success) + return ErrorResult(code!, message!); + + return Ok(new ApiResponse(true, data)); + } + + [HttpPost("refresh")] + public async Task Refresh([FromBody] RefreshTokenRequest request, CancellationToken cancellationToken) + { + var validation = await _refreshValidator.ValidateAsync(request, cancellationToken); + if (!validation.IsValid) + return BadRequest(ValidationError(validation)); + + var (success, data, code, message) = await _auth.RefreshAsync(request, cancellationToken); + if (!success) + return ErrorResult(code!, message!); + + return Ok(new ApiResponse(true, data)); + } + + private static ApiResponse ValidationError(FluentValidation.Results.ValidationResult validation) + { + var first = validation.Errors.First(); + return new ApiResponse(false, null, new ApiError("VALIDATION_ERROR", first.ErrorMessage, first.PropertyName)); + } + + private IActionResult ErrorResult(string code, string message) => + code switch + { + "NOT_FOUND" => NotFound(new ApiResponse(false, null, new ApiError(code, message))), + "RATE_LIMITED" => StatusCode(429, new ApiResponse(false, null, new ApiError(code, message))), + _ => BadRequest(new ApiResponse(false, null, new ApiError(code, message))) + }; +} diff --git a/src/Meezi.Admin.API/Controllers/AdminCafesController.cs b/src/Meezi.Admin.API/Controllers/AdminCafesController.cs new file mode 100644 index 0000000..a8d1891 --- /dev/null +++ b/src/Meezi.Admin.API/Controllers/AdminCafesController.cs @@ -0,0 +1,70 @@ +using Microsoft.AspNetCore.Mvc; +using Meezi.Admin.API.Models; +using Meezi.Admin.API.Services; +using Meezi.Shared; + +namespace Meezi.Admin.API.Controllers; + +[Route("api/admin/cafes")] +public class AdminCafesController : AdminApiControllerBase +{ + private readonly IAdminPlatformService _platform; + + public AdminCafesController(IAdminPlatformService platform) + { + _platform = platform; + } + + [HttpGet] + public async Task List(CancellationToken cancellationToken) + { + var cafes = await _platform.ListCafesAsync(cancellationToken); + return Ok(new ApiResponse(true, cafes)); + } + + [HttpPatch("{cafeId}")] + public async Task Patch(string cafeId, [FromBody] AdminCafePatchRequest request, CancellationToken cancellationToken) + { + var ok = await _platform.PatchCafeAsync(cafeId, request, cancellationToken); + if (!ok) + return NotFound(new ApiResponse(false, null, new ApiError("NOT_FOUND", "Cafe not found."))); + + return Ok(new ApiResponse(true, new { cafeId })); + } + + [HttpPut("{cafeId}/features")] + public async Task SetFeature( + string cafeId, + [FromBody] CafeFeatureOverrideRequest request, + CancellationToken cancellationToken) + { + var ok = await _platform.SetCafeFeatureOverrideAsync(cafeId, request, cancellationToken); + if (!ok) + return NotFound(new ApiResponse(false, null, new ApiError("NOT_FOUND", "Cafe not found."))); + + return Ok(new ApiResponse(true, new { cafeId, request.FeatureKey, request.IsEnabled })); + } + + [HttpGet("{cafeId}/discover-profile")] + public async Task GetDiscoverProfile(string cafeId, CancellationToken cancellationToken) + { + var data = await _platform.GetCafeDiscoverProfileAsync(cafeId, cancellationToken); + if (data is null) + return NotFound(new ApiResponse(false, null, new ApiError("NOT_FOUND", "Cafe not found."))); + + return Ok(new ApiResponse(true, data)); + } + + [HttpPut("{cafeId}/discover-profile")] + public async Task PutDiscoverProfile( + string cafeId, + [FromBody] AdminUpsertCafeDiscoverProfileRequest request, + CancellationToken cancellationToken) + { + var data = await _platform.UpsertCafeDiscoverProfileAsync(cafeId, request, cancellationToken); + if (data is null) + return NotFound(new ApiResponse(false, null, new ApiError("NOT_FOUND", "Cafe not found."))); + + return Ok(new ApiResponse(true, data)); + } +} diff --git a/src/Meezi.Admin.API/Controllers/AdminDashboardController.cs b/src/Meezi.Admin.API/Controllers/AdminDashboardController.cs new file mode 100644 index 0000000..2cb85ed --- /dev/null +++ b/src/Meezi.Admin.API/Controllers/AdminDashboardController.cs @@ -0,0 +1,23 @@ +using Microsoft.AspNetCore.Mvc; +using Meezi.Admin.API.Services; +using Meezi.Shared; + +namespace Meezi.Admin.API.Controllers; + +[Route("api/admin/dashboard")] +public class AdminDashboardController : AdminApiControllerBase +{ + private readonly IAdminPlatformService _platform; + + public AdminDashboardController(IAdminPlatformService platform) + { + _platform = platform; + } + + [HttpGet("stats")] + public async Task GetStats(CancellationToken cancellationToken) + { + var stats = await _platform.GetDashboardStatsAsync(cancellationToken); + return Ok(new ApiResponse(true, stats)); + } +} diff --git a/src/Meezi.Admin.API/Controllers/AdminFeaturesController.cs b/src/Meezi.Admin.API/Controllers/AdminFeaturesController.cs new file mode 100644 index 0000000..f166830 --- /dev/null +++ b/src/Meezi.Admin.API/Controllers/AdminFeaturesController.cs @@ -0,0 +1,34 @@ +using Microsoft.AspNetCore.Mvc; +using Meezi.Admin.API.Models; +using Meezi.Admin.API.Services; +using Meezi.Shared; + +namespace Meezi.Admin.API.Controllers; + +[Route("api/admin/features")] +public class AdminFeaturesController : AdminApiControllerBase +{ + private readonly IAdminPlatformService _platform; + + public AdminFeaturesController(IAdminPlatformService platform) + { + _platform = platform; + } + + [HttpGet] + public async Task List(CancellationToken cancellationToken) + { + var features = await _platform.GetFeaturesAsync(cancellationToken); + return Ok(new ApiResponse(true, features)); + } + + [HttpPatch("{featureKey}")] + public async Task Update(string featureKey, [FromBody] UpdateFeatureRequest request, CancellationToken cancellationToken) + { + var ok = await _platform.UpdateFeatureAsync(featureKey, request, cancellationToken); + if (!ok) + return NotFound(new ApiResponse(false, null, new ApiError("NOT_FOUND", "Feature not found."))); + + return Ok(new ApiResponse(true, new { featureKey })); + } +} diff --git a/src/Meezi.Admin.API/Controllers/AdminIntegrationsController.cs b/src/Meezi.Admin.API/Controllers/AdminIntegrationsController.cs new file mode 100644 index 0000000..c02eee2 --- /dev/null +++ b/src/Meezi.Admin.API/Controllers/AdminIntegrationsController.cs @@ -0,0 +1,34 @@ +using Microsoft.AspNetCore.Mvc; +using Meezi.Admin.API.Models; +using Meezi.Admin.API.Services; +using Meezi.Shared; + +namespace Meezi.Admin.API.Controllers; + +[Route("api/admin/integrations")] +public class AdminIntegrationsController : AdminApiControllerBase +{ + private readonly IPlatformIntegrationService _integrations; + + public AdminIntegrationsController(IPlatformIntegrationService integrations) + { + _integrations = integrations; + } + + [HttpGet] + public async Task Get(CancellationToken cancellationToken) + { + var data = await _integrations.GetIntegrationsAsync(cancellationToken); + return Ok(new ApiResponse(true, data)); + } + + [HttpPut] + public async Task Save( + [FromBody] UpdatePlatformIntegrationsRequest request, + CancellationToken cancellationToken) + { + await _integrations.SaveIntegrationsAsync(request, cancellationToken); + var data = await _integrations.GetIntegrationsAsync(cancellationToken); + return Ok(new ApiResponse(true, data)); + } +} diff --git a/src/Meezi.Admin.API/Controllers/AdminNotificationsController.cs b/src/Meezi.Admin.API/Controllers/AdminNotificationsController.cs new file mode 100644 index 0000000..69b4bf8 --- /dev/null +++ b/src/Meezi.Admin.API/Controllers/AdminNotificationsController.cs @@ -0,0 +1,57 @@ +using Microsoft.AspNetCore.Mvc; +using Meezi.Admin.API.Models; +using Meezi.Admin.API.Services; +using Meezi.Core.Interfaces; +using Meezi.Shared; + +namespace Meezi.Admin.API.Controllers; + +[Route("api/admin/notifications")] +public class AdminNotificationsController : AdminApiControllerBase +{ + private readonly IAdminNotificationService _notifications; + + public AdminNotificationsController(IAdminNotificationService notifications) + { + _notifications = notifications; + } + + [HttpGet] + public async Task List( + [FromQuery] int limit = 50, + [FromQuery] string? cafeId = null, + CancellationToken cancellationToken = default) + { + var items = await _notifications.ListAsync(limit, cafeId, cancellationToken); + return Ok(new ApiResponse(true, new { items, total = items.Count })); + } + + [HttpPost("broadcast")] + public async Task Broadcast( + [FromBody] BroadcastNotificationRequest request, + ITenantContext tenant, + CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(request.Title)) + return BadRequest(new ApiResponse(false, null, new ApiError("VALIDATION", "Title is required."))); + + var adminId = RequireAdminId(tenant); + var result = await _notifications.BroadcastAsync( + request.Title, + request.Body ?? string.Empty, + adminId, + cancellationToken); + + return Ok(new ApiResponse(true, result)); + } + + [HttpDelete("{id}")] + public async Task Delete(string id, CancellationToken cancellationToken) + { + var ok = await _notifications.DeleteAsync(id, cancellationToken); + if (!ok) + return NotFound(new ApiResponse(false, null, new ApiError("NOT_FOUND", "Notification not found."))); + + return Ok(new ApiResponse(true, new { id })); + } +} diff --git a/src/Meezi.Admin.API/Controllers/AdminPlansController.cs b/src/Meezi.Admin.API/Controllers/AdminPlansController.cs new file mode 100644 index 0000000..42e3605 --- /dev/null +++ b/src/Meezi.Admin.API/Controllers/AdminPlansController.cs @@ -0,0 +1,37 @@ +using Microsoft.AspNetCore.Mvc; +using Meezi.Admin.API.Models; +using Meezi.Admin.API.Services; +using Meezi.Core.Enums; +using Meezi.Shared; + +namespace Meezi.Admin.API.Controllers; + +[Route("api/admin/plans")] +public class AdminPlansController : AdminApiControllerBase +{ + private readonly IAdminPlatformService _platform; + + public AdminPlansController(IAdminPlatformService platform) + { + _platform = platform; + } + + [HttpGet] + public async Task List(CancellationToken cancellationToken) + { + var plans = await _platform.GetPlansAsync(cancellationToken); + return Ok(new ApiResponse(true, plans)); + } + + [HttpPut("{tier}")] + public async Task Update(PlanTier tier, [FromBody] UpdatePlanRequest request, CancellationToken cancellationToken) + { + var ok = await _platform.UpdatePlanAsync(tier, request, cancellationToken); + if (!ok) + return NotFound(new ApiResponse(false, null, new ApiError("NOT_FOUND", "Plan not found."))); + + var plans = await _platform.GetPlansAsync(cancellationToken); + var updated = plans.FirstOrDefault(p => p.Tier == tier); + return Ok(new ApiResponse(true, updated)); + } +} diff --git a/src/Meezi.Admin.API/Controllers/AdminSettingsController.cs b/src/Meezi.Admin.API/Controllers/AdminSettingsController.cs new file mode 100644 index 0000000..0392ce3 --- /dev/null +++ b/src/Meezi.Admin.API/Controllers/AdminSettingsController.cs @@ -0,0 +1,34 @@ +using Microsoft.AspNetCore.Mvc; +using Meezi.Admin.API.Models; +using Meezi.Admin.API.Services; +using Meezi.Shared; + +namespace Meezi.Admin.API.Controllers; + +[Route("api/admin/settings")] +public class AdminSettingsController : AdminApiControllerBase +{ + private readonly IAdminPlatformService _platform; + + public AdminSettingsController(IAdminPlatformService platform) + { + _platform = platform; + } + + [HttpGet] + public async Task List(CancellationToken cancellationToken) + { + var settings = await _platform.GetSettingsAsync(cancellationToken); + return Ok(new ApiResponse(true, settings)); + } + + [HttpPatch("{key}")] + public async Task Update(string key, [FromBody] UpdateSettingRequest request, CancellationToken cancellationToken) + { + var ok = await _platform.UpdateSettingAsync(key, request, cancellationToken); + if (!ok) + return NotFound(new ApiResponse(false, null, new ApiError("NOT_FOUND", "Setting not found."))); + + return Ok(new ApiResponse(true, new { key, request.Value })); + } +} diff --git a/src/Meezi.Admin.API/Controllers/AdminTicketsController.cs b/src/Meezi.Admin.API/Controllers/AdminTicketsController.cs new file mode 100644 index 0000000..2d01656 --- /dev/null +++ b/src/Meezi.Admin.API/Controllers/AdminTicketsController.cs @@ -0,0 +1,72 @@ +using Microsoft.AspNetCore.Mvc; +using Meezi.Admin.API.Models; +using Meezi.Infrastructure.Models; +using Meezi.Infrastructure.Services; +using Meezi.Core.Enums; +using Meezi.Core.Interfaces; +using Meezi.Shared; + +namespace Meezi.Admin.API.Controllers; + +[Route("api/admin/tickets")] +public class AdminTicketsController : AdminApiControllerBase +{ + private readonly ISupportTicketService _tickets; + + public AdminTicketsController(ISupportTicketService tickets) + { + _tickets = tickets; + } + + [HttpGet] + public async Task List([FromQuery] SupportTicketStatus? status, CancellationToken cancellationToken) + { + var list = await _tickets.ListAllAsync(status, cancellationToken); + return Ok(new ApiResponse(true, list)); + } + + [HttpGet("{ticketId}")] + public async Task Get(string ticketId, CancellationToken cancellationToken) + { + var detail = await _tickets.GetAdminAsync(ticketId, cancellationToken); + if (detail is null) + return NotFound(new ApiResponse(false, null, new ApiError("NOT_FOUND", "Ticket not found."))); + + return Ok(new ApiResponse(true, detail)); + } + + [HttpPost("{ticketId}/messages")] + public async Task Reply( + string ticketId, + [FromBody] ReplySupportTicketRequest request, + ITenantContext tenant, + CancellationToken cancellationToken) + { + var existing = await _tickets.GetAdminAsync(ticketId, cancellationToken); + if (existing is null) + return NotFound(new ApiResponse(false, null, new ApiError("NOT_FOUND", "Ticket not found."))); + if (existing.Ticket.Status is SupportTicketStatus.Closed) + return BadRequest(new ApiResponse(false, null, + new ApiError("TICKET_CLOSED", "This ticket is closed."))); + + var adminId = RequireAdminId(tenant); + var detail = await _tickets.ReplyAsAdminAsync(ticketId, adminId, request, cancellationToken); + if (detail is null) + return NotFound(new ApiResponse(false, null, new ApiError("NOT_FOUND", "Ticket not found."))); + + return Ok(new ApiResponse(true, detail)); + } + + [HttpPatch("{ticketId}")] + public async Task Update( + string ticketId, + [FromBody] UpdateSupportTicketRequest request, + CancellationToken cancellationToken) + { + var detail = await _tickets.UpdateAdminAsync(ticketId, request, cancellationToken); + if (detail is null) + return NotFound(new ApiResponse(false, null, new ApiError("NOT_FOUND", "Ticket not found."))); + + return Ok(new ApiResponse(true, detail)); + } +} diff --git a/src/Meezi.Admin.API/Controllers/AdminWebsiteController.cs b/src/Meezi.Admin.API/Controllers/AdminWebsiteController.cs new file mode 100644 index 0000000..aea79d1 --- /dev/null +++ b/src/Meezi.Admin.API/Controllers/AdminWebsiteController.cs @@ -0,0 +1,140 @@ +using Microsoft.AspNetCore.Mvc; +using Meezi.Core.Interfaces; +using Meezi.Admin.API.Services; +using Meezi.Shared; + +namespace Meezi.Admin.API.Controllers; + +[Route("api/admin/website")] +public class AdminWebsiteController(IAdminWebsiteService websiteAdmin) : AdminApiControllerBase +{ + // ── Blog posts ──────────────────────────────────────────────────────── + + [HttpGet("posts")] + public async Task ListPosts( + [FromQuery] int page = 1, + [FromQuery] int limit = 20, + [FromQuery] bool? published = null, + CancellationToken ct = default) + { + var result = await websiteAdmin.ListPostsAsync(page, limit, published, ct); + return Ok(new ApiResponse(true, result)); + } + + [HttpGet("posts/{id}")] + public async Task GetPost(string id, CancellationToken ct = default) + { + var post = await websiteAdmin.GetPostAsync(id, ct); + if (post is null) return NotFound(new ApiResponse(false, null, + new ApiError("NOT_FOUND", "Post not found."))); + return Ok(new ApiResponse(true, post)); + } + + [HttpPost("posts")] + public async Task CreatePost([FromBody] UpsertPostRequest req, CancellationToken ct = default) + { + var post = await websiteAdmin.CreatePostAsync(req, ct); + return Ok(new ApiResponse(true, post)); + } + + [HttpPut("posts/{id}")] + public async Task UpdatePost(string id, + [FromBody] UpsertPostRequest req, CancellationToken ct = default) + { + var post = await websiteAdmin.UpdatePostAsync(id, req, ct); + if (post is null) return NotFound(new ApiResponse(false, null, + new ApiError("NOT_FOUND", "Post not found."))); + return Ok(new ApiResponse(true, post)); + } + + [HttpDelete("posts/{id}")] + public async Task DeletePost(string id, CancellationToken ct = default) + { + await websiteAdmin.DeletePostAsync(id, ct); + return Ok(new ApiResponse(true, null)); + } + + [HttpPatch("posts/{id}/publish")] + public async Task PublishPost(string id, CancellationToken ct = default) + { + await websiteAdmin.SetPublishedAsync(id, true, ct); + return Ok(new ApiResponse(true, null)); + } + + [HttpPatch("posts/{id}/unpublish")] + public async Task UnpublishPost(string id, CancellationToken ct = default) + { + await websiteAdmin.SetPublishedAsync(id, false, ct); + return Ok(new ApiResponse(true, null)); + } + + // ── Comments ────────────────────────────────────────────────────────── + + [HttpGet("comments")] + public async Task ListComments( + [FromQuery] bool? approved = null, + [FromQuery] int page = 1, + [FromQuery] int limit = 30, + CancellationToken ct = default) + { + var result = await websiteAdmin.ListCommentsAsync(approved, page, limit, ct); + return Ok(new ApiResponse(true, result)); + } + + [HttpPatch("comments/{id}/approve")] + public async Task ApproveComment(string id, CancellationToken ct = default) + { + await websiteAdmin.SetCommentApprovedAsync(id, true, ct); + return Ok(new ApiResponse(true, null)); + } + + [HttpPatch("comments/{id}/reject")] + public async Task RejectComment(string id, CancellationToken ct = default) + { + await websiteAdmin.SetCommentApprovedAsync(id, false, ct); + return Ok(new ApiResponse(true, null)); + } + + [HttpDelete("comments/{id}")] + public async Task DeleteComment(string id, CancellationToken ct = default) + { + await websiteAdmin.DeleteCommentAsync(id, ct); + return Ok(new ApiResponse(true, null)); + } + + // ── Demo requests ───────────────────────────────────────────────────── + + [HttpGet("demo-requests")] + public async Task ListDemoRequests( + [FromQuery] string? status = null, + [FromQuery] int page = 1, + [FromQuery] int limit = 30, + CancellationToken ct = default) + { + var result = await websiteAdmin.ListDemoRequestsAsync(status, page, limit, ct); + return Ok(new ApiResponse(true, result)); + } + + [HttpPatch("demo-requests/{id}/status")] + public async Task UpdateDemoStatus( + string id, + [FromBody] UpdateDemoStatusRequest req, + CancellationToken ct = default) + { + await websiteAdmin.UpdateDemoStatusAsync(id, req.Status, req.AdminNotes, ct); + return Ok(new ApiResponse(true, null)); + } +} + +public record UpsertPostRequest( + string Slug, + string TitleFa, string? TitleEn, + string? ExcerptFa, string? ExcerptEn, + string ContentFa, string? ContentEn, + string? CategoryFa, string? CategoryEn, + string? Author, + string? TagsJson, + string? CoverImage, + bool IsPublished); + +public record UpdateDemoStatusRequest(string Status, string? AdminNotes); diff --git a/src/Meezi.Admin.API/Extensions/AdminServiceCollectionExtensions.cs b/src/Meezi.Admin.API/Extensions/AdminServiceCollectionExtensions.cs new file mode 100644 index 0000000..60845b6 --- /dev/null +++ b/src/Meezi.Admin.API/Extensions/AdminServiceCollectionExtensions.cs @@ -0,0 +1,104 @@ +using System.Text; +using System.Text.Json.Serialization; +using FluentValidation; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; +using Meezi.Admin.API.Hubs; +using Meezi.Admin.API.Services; +using Meezi.Admin.API.Validators; +using Meezi.Infrastructure; +using Serilog; +using StackExchange.Redis; + +namespace Meezi.Admin.API.Extensions; + +public static class AdminServiceCollectionExtensions +{ + public static IServiceCollection AddMeeziAdminServices( + this IServiceCollection services, + IConfiguration configuration) + { + services.AddInfrastructure(configuration); + + services.AddScoped(); + services.AddScoped(); + services.AddSingleton(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + services.AddControllers() + .AddJsonOptions(options => + { + options.JsonSerializerOptions.PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase; + options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); + }); + services.AddEndpointsApiExplorer(); + services.AddSwaggerGen(); + services.AddSignalR(); + services.AddValidatorsFromAssemblyContaining(); + + var jwtKey = configuration["Jwt:Key"] ?? "meezi-dev-secret-key-min-32-chars!!"; + var jwtIssuer = configuration["Jwt:Issuer"] ?? "meezi"; + var jwtAudience = configuration["Jwt:Audience"] ?? "meezi-admin"; + + services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(options => + { + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = jwtIssuer, + ValidAudience = jwtAudience, + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey)) + }; + }); + + services.AddAuthorization(); + + var redisConnection = configuration.GetConnectionString("Redis") ?? "localhost:6379"; + services.AddSingleton(_ => + ConnectionMultiplexer.Connect($"{redisConnection},abortConnect=false")); + + services.AddCors(options => + { + options.AddPolicy("AdminCors", policy => + { + var origins = configuration.GetSection("Cors:Origins").Get() + ?? ["http://localhost:3102"]; + policy.WithOrigins(origins) + .AllowAnyHeader() + .AllowAnyMethod() + .AllowCredentials(); + }); + }); + + return services; + } + + public static WebApplication ConfigureMeeziAdminPipeline(this WebApplication app) + { + app.UseSerilogRequestLogging(); + + if (app.Environment.IsDevelopment()) + { + app.UseSwagger(); + app.UseSwaggerUI(); + } + + app.UseHttpsRedirection(); + app.UseCors("AdminCors"); + app.UseAuthentication(); + app.UseMiddleware(); + app.UseAuthorization(); + app.MapControllers(); + app.MapHub("/hubs/kds"); + app.MapGet("/health", () => Results.Ok(new { status = "healthy", service = "meezi-admin-api" })); + + return app; + } +} diff --git a/src/Meezi.Admin.API/Hubs/KdsHub.cs b/src/Meezi.Admin.API/Hubs/KdsHub.cs new file mode 100644 index 0000000..7091555 --- /dev/null +++ b/src/Meezi.Admin.API/Hubs/KdsHub.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.SignalR; + +namespace Meezi.Admin.API.Hubs; + +/// Shared hub name with merchant API so café dashboards receive platform broadcasts. +[Authorize(Roles = "SystemAdmin")] +public class KdsHub : Hub +{ + public static string GroupName(string cafeId) => $"cafe:{cafeId}"; +} diff --git a/src/Meezi.Admin.API/Meezi.Admin.API.csproj b/src/Meezi.Admin.API/Meezi.Admin.API.csproj new file mode 100644 index 0000000..143ac4d --- /dev/null +++ b/src/Meezi.Admin.API/Meezi.Admin.API.csproj @@ -0,0 +1,28 @@ + + + + Meezi.Admin.API + Meezi.Admin.API + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + diff --git a/src/Meezi.Admin.API/Middleware/AdminTenantMiddleware.cs b/src/Meezi.Admin.API/Middleware/AdminTenantMiddleware.cs new file mode 100644 index 0000000..9183e09 --- /dev/null +++ b/src/Meezi.Admin.API/Middleware/AdminTenantMiddleware.cs @@ -0,0 +1,63 @@ +using System.Text.Json; +using Meezi.Core.Constants; +using Meezi.Core.Interfaces; +using Meezi.Infrastructure.Data; +using Meezi.Shared; + +namespace Meezi.Admin.API.Middleware; + +public class AdminTenantMiddleware +{ + private static readonly string[] PublicPrefixes = + [ + "/api/admin/auth", + "/health", + "/swagger" + ]; + + private readonly RequestDelegate _next; + + public AdminTenantMiddleware(RequestDelegate next) => _next = next; + + public async Task InvokeAsync(HttpContext context, ITenantContext tenant) + { + if (IsPublicPath(context.Request.Path)) + { + await _next(context); + return; + } + + if (context.User.Identity?.IsAuthenticated != true) + { + await WriteUnauthorizedAsync(context, "UNAUTHORIZED", "Authentication required."); + return; + } + + var actor = context.User.FindFirst(MeeziClaimTypes.Actor)?.Value; + if (actor != MeeziActorKinds.SystemAdmin) + { + await WriteUnauthorizedAsync(context, "FORBIDDEN", "System admin access required."); + return; + } + + if (tenant is Infrastructure.Data.TenantContext scoped) + { + scoped.UserId = context.User.FindFirst("sub")?.Value + ?? context.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; + scoped.Language = context.User.FindFirst(MeeziClaimTypes.Language)?.Value ?? "fa"; + scoped.IsSystemAdmin = true; + } + + await _next(context); + } + + private static bool IsPublicPath(PathString path) => + PublicPrefixes.Any(p => path.StartsWithSegments(p, StringComparison.OrdinalIgnoreCase)); + + private static async Task WriteUnauthorizedAsync(HttpContext context, string code, string message) + { + context.Response.StatusCode = StatusCodes.Status401Unauthorized; + context.Response.ContentType = "application/json"; + await context.Response.WriteAsync(JsonSerializer.Serialize(new ApiResponse(false, null, new ApiError(code, message)))); + } +} diff --git a/src/Meezi.Admin.API/Models/AdminDtos.cs b/src/Meezi.Admin.API/Models/AdminDtos.cs new file mode 100644 index 0000000..d45a874 --- /dev/null +++ b/src/Meezi.Admin.API/Models/AdminDtos.cs @@ -0,0 +1,51 @@ +using Meezi.Core.Enums; +using Meezi.Core.Platform; + +namespace Meezi.Admin.API.Models; + +public record AdminDashboardStatsDto( + int TotalCafes, + int ActiveCafes, + int SuspendedCafes, + int OpenTickets, + int PlansConfigured); + +public record UpdatePlanRequest( + string DisplayNameFa, + string? DisplayNameEn, + decimal MonthlyPriceToman, + bool IsBillableOnline, + bool IsActive, + int SortOrder, + PlanLimitsData Limits, + IReadOnlyList? FeatureKeys); + +public record UpdateSettingRequest(string Value, string? DescriptionFa); + +public record UpdateFeatureRequest( + string DisplayNameFa, + string? DisplayNameEn, + string ModuleGroup, + bool IsEnabledGlobally); + +public record AdminCafeListItemDto( + string Id, + string Name, + string Slug, + string City, + PlanTier PlanTier, + DateTime? PlanExpiresAt, + bool IsSuspended, + bool IsVerified, + int BranchCount, + int EmployeeCount, + DateTime CreatedAt); + +public record AdminCafePatchRequest( + PlanTier? PlanTier, + DateTime? PlanExpiresAt, + bool? IsSuspended, + bool? IsVerified, + IReadOnlyList? DiscoverBadges = null); + +public record CafeFeatureOverrideRequest(string FeatureKey, bool IsEnabled); diff --git a/src/Meezi.Admin.API/Models/AuthDtos.cs b/src/Meezi.Admin.API/Models/AuthDtos.cs new file mode 100644 index 0000000..2e5d034 --- /dev/null +++ b/src/Meezi.Admin.API/Models/AuthDtos.cs @@ -0,0 +1,22 @@ +using Meezi.Core.Constants; + +namespace Meezi.Admin.API.Models; + +public record SendOtpRequest(string Phone); + +public record VerifyOtpRequest(string Phone, string Code); + +public record RefreshTokenRequest(string RefreshToken); + +public record AuthTokenResponse( + string AccessToken, + string RefreshToken, + DateTime ExpiresAt, + string UserId, + string CafeId, + string Role, + string PlanTier, + string Language, + string Actor = MeeziActorKinds.SystemAdmin); + +public record SendOtpResponse(bool Sent, int ExpiresInSeconds); diff --git a/src/Meezi.Admin.API/Models/DiscoverProfileDtos.cs b/src/Meezi.Admin.API/Models/DiscoverProfileDtos.cs new file mode 100644 index 0000000..989a20d --- /dev/null +++ b/src/Meezi.Admin.API/Models/DiscoverProfileDtos.cs @@ -0,0 +1,23 @@ +namespace Meezi.Admin.API.Models; + +public record AdminCafeDiscoverProfileDto( + string CafeId, + string CafeName, + IReadOnlyList Themes, + string? Size, + string? Floors, + IReadOnlyList Vibes, + IReadOnlyList Occasions, + IReadOnlyList SpaceFeatures, + string? NoiseLevel, + string? PriceTier); + +public record AdminUpsertCafeDiscoverProfileRequest( + IReadOnlyList? Themes, + string? Size, + string? Floors, + IReadOnlyList? Vibes, + IReadOnlyList? Occasions, + IReadOnlyList? SpaceFeatures, + string? NoiseLevel, + string? PriceTier); diff --git a/src/Meezi.Admin.API/Models/IntegrationDtos.cs b/src/Meezi.Admin.API/Models/IntegrationDtos.cs new file mode 100644 index 0000000..62a19a0 --- /dev/null +++ b/src/Meezi.Admin.API/Models/IntegrationDtos.cs @@ -0,0 +1,109 @@ +namespace Meezi.Admin.API.Models; + +public record GatewayCredentialsDto( + string? Username, + string? Password, + string? BranchCode, + string? TerminalCode, + string? ClientId, + string? ClientSecret, + string? BaseUrl, + bool HasStoredPassword, + bool HasStoredClientSecret); + +public record PaymentGatewayConfigDto( + string Id, + string DisplayNameFa, + bool IsEnabled, + bool IsActive, + string? MerchantId, + string? ApiKey, + bool Sandbox, + bool HasStoredSecret, + GatewayCredentialsDto? Credentials = null); + +public record KavenegarConfigDto( + bool IsEnabled, + string? ApiKey, + string OtpTemplate, + bool HasStoredApiKey); + +public record OpenAiIntegrationConfigDto( + bool IsEnabled, + string? ApiKey, + string Model, + bool CoffeeAdvisorEnabled, + bool HasStoredApiKey); + +public record MeshyIntegrationConfigDto( + bool IsEnabled, + string? ApiKey, + bool Menu3dEnabled, + bool HasStoredApiKey); + +public record AiIntegrationsConfigDto( + OpenAiIntegrationConfigDto OpenAi, + MeshyIntegrationConfigDto Meshy); + +public record PlatformIntegrationsDto( + string ActivePaymentGateway, + IReadOnlyList PaymentGateways, + KavenegarConfigDto Kavenegar, + AiIntegrationsConfigDto Ai); + +public record UpdatePlatformIntegrationsRequest( + string ActivePaymentGateway, + IReadOnlyList PaymentGateways, + UpdateKavenegarRequest Kavenegar, + UpdateAiIntegrationsRequest Ai); + +public record UpdateOpenAiIntegrationRequest( + bool IsEnabled, + string? ApiKey, + string Model, + bool CoffeeAdvisorEnabled); + +public record UpdateMeshyIntegrationRequest( + bool IsEnabled, + string? ApiKey, + bool Menu3dEnabled); + +public record UpdateAiIntegrationsRequest( + UpdateOpenAiIntegrationRequest OpenAi, + UpdateMeshyIntegrationRequest Meshy); + +public record UpdatePaymentGatewayCredentialsRequest( + string? Username, + string? Password, + string? BranchCode, + string? TerminalCode, + string? ClientId, + string? ClientSecret, + string? BaseUrl); + +public record UpdatePaymentGatewayRequest( + string Id, + bool IsEnabled, + string? MerchantId, + string? ApiKey, + bool Sandbox, + UpdatePaymentGatewayCredentialsRequest? Credentials = null); + +public record UpdateKavenegarRequest( + bool IsEnabled, + string? ApiKey, + string OtpTemplate); + +public record AdminNotificationRowDto( + string Id, + string CafeId, + string CafeName, + string Type, + string Title, + string? Body, + bool IsRead, + DateTime CreatedAt); + +public record BroadcastNotificationRequest(string Title, string Body); + +public record BroadcastNotificationResult(int CafeCount, int NotificationCount); diff --git a/src/Meezi.Admin.API/Models/NotificationDtos.cs b/src/Meezi.Admin.API/Models/NotificationDtos.cs new file mode 100644 index 0000000..c117e83 --- /dev/null +++ b/src/Meezi.Admin.API/Models/NotificationDtos.cs @@ -0,0 +1,11 @@ +namespace Meezi.Admin.API.Models; + +public record CafeNotificationDto( + string Id, + string Type, + string Title, + string? Body, + string? ReferenceId, + string? TableNumber, + bool IsRead, + DateTime CreatedAt); diff --git a/src/Meezi.Admin.API/Program.cs b/src/Meezi.Admin.API/Program.cs new file mode 100644 index 0000000..e387fba --- /dev/null +++ b/src/Meezi.Admin.API/Program.cs @@ -0,0 +1,51 @@ +using Microsoft.EntityFrameworkCore; +using Meezi.Admin.API.Extensions; +using Meezi.Infrastructure.Data; +using Serilog; + +namespace Meezi.Admin.API; + +public class Program +{ + public static async Task Main(string[] args) + { + Log.Logger = new LoggerConfiguration() + .WriteTo.Console() + .CreateLogger(); + + try + { + var builder = WebApplication.CreateBuilder(args); + builder.Host.UseSerilog((context, services, configuration) => configuration + .ReadFrom.Configuration(context.Configuration) + .ReadFrom.Services(services) + .Enrich.FromLogContext() + .WriteTo.Console()); + + builder.Services.AddMeeziAdminServices(builder.Configuration); + var app = builder.Build(); + app.ConfigureMeeziAdminPipeline(); + + if (app.Configuration.GetValue("RUN_MIGRATIONS")) + { + await using var scope = app.Services.CreateAsyncScope(); + var db = scope.ServiceProvider.GetRequiredService(); + await db.Database.MigrateAsync(); + await DatabaseSchemaPatches.ApplyAsync(db); + } + + if (!app.Configuration.GetValue("Testing:SkipSeed")) + await PlatformDataSeeder.SeedAsync(app.Services); + + await app.RunAsync(); + } + catch (Exception ex) + { + Log.Fatal(ex, "Admin API terminated unexpectedly"); + } + finally + { + await Log.CloseAndFlushAsync(); + } + } +} diff --git a/src/Meezi.Admin.API/Properties/launchSettings.json b/src/Meezi.Admin.API/Properties/launchSettings.json new file mode 100644 index 0000000..6bc0b1a --- /dev/null +++ b/src/Meezi.Admin.API/Properties/launchSettings.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7210;http://localhost:5210", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/Meezi.Admin.API/Services/AdminAuthService.cs b/src/Meezi.Admin.API/Services/AdminAuthService.cs new file mode 100644 index 0000000..32b3e88 --- /dev/null +++ b/src/Meezi.Admin.API/Services/AdminAuthService.cs @@ -0,0 +1,178 @@ +using Meezi.Admin.API.Models; +using Meezi.Core.Constants; +using Meezi.Core.Enums; +using Meezi.Core.Interfaces; +using Meezi.Core.Utilities; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using StackExchange.Redis; + +namespace Meezi.Admin.API.Services; + +public interface IAdminAuthService +{ + Task<(bool Success, SendOtpResponse? Data, string? ErrorCode, string? ErrorMessage)> SendOtpAsync( + SendOtpRequest request, + CancellationToken cancellationToken = default); + + Task<(bool Success, AuthTokenResponse? Data, string? ErrorCode, string? ErrorMessage)> VerifyOtpAsync( + VerifyOtpRequest request, + CancellationToken cancellationToken = default); + + Task<(bool Success, AuthTokenResponse? Data, string? ErrorCode, string? ErrorMessage)> RefreshAsync( + RefreshTokenRequest request, + CancellationToken cancellationToken = default); +} + +public class AdminAuthService : IAdminAuthService +{ + private const int OtpTtlSeconds = 300; + private const int DefaultMaxOtpAttemptsPerHour = 5; + + private readonly AppDbContext _db; + private readonly IConnectionMultiplexer _redis; + private readonly ISmsService _smsService; + private readonly IAdminJwtTokenService _jwtTokenService; + private readonly IRefreshTokenStore _refreshTokenStore; + private readonly IConfiguration _configuration; + private readonly ILogger _logger; + + public AdminAuthService( + AppDbContext db, + IConnectionMultiplexer redis, + ISmsService smsService, + IAdminJwtTokenService jwtTokenService, + IRefreshTokenStore refreshTokenStore, + IConfiguration configuration, + ILogger logger) + { + _db = db; + _redis = redis; + _smsService = smsService; + _jwtTokenService = jwtTokenService; + _refreshTokenStore = refreshTokenStore; + _configuration = configuration; + _logger = logger; + } + + public async Task<(bool Success, SendOtpResponse? Data, string? ErrorCode, string? ErrorMessage)> SendOtpAsync( + SendOtpRequest request, + CancellationToken cancellationToken = default) + { + var phone = PhoneNormalizer.Normalize(request.Phone); + var admin = await _db.SystemAdmins + .FirstOrDefaultAsync(a => a.Phone == phone && a.IsActive && a.DeletedAt == null, cancellationToken); + + if (admin is null) + return (false, null, "NOT_FOUND", "No system admin account for this phone."); + + var redis = _redis.GetDatabase(); + var maxAttempts = _configuration.GetValue("Auth:MaxOtpAttemptsPerHour", DefaultMaxOtpAttemptsPerHour); + var attemptsKey = $"otp:admin:{phone}"; + if (maxAttempts > 0) + { + var attempts = await redis.StringGetAsync(attemptsKey); + if (attempts.HasValue && (int)attempts >= maxAttempts) + return (false, null, "RATE_LIMITED", "Too many OTP requests. Try again later."); + } + + var otp = Random.Shared.Next(100000, 999999).ToString(); + await redis.StringSetAsync($"otp:admin:{phone}", otp, TimeSpan.FromSeconds(OtpTtlSeconds)); + + if (string.IsNullOrWhiteSpace(_configuration["Kavenegar:ApiKey"])) + _logger.LogWarning("DEV admin OTP for {Phone}: {Otp}", phone, otp); + + try + { + await _smsService.SendOtpAsync(phone, otp, cancellationToken); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to send admin OTP SMS"); + return (false, null, "SMS_FAILED", "Could not send verification code."); + } + + if (maxAttempts > 0) + { + var newAttempts = await redis.StringIncrementAsync(attemptsKey); + if (newAttempts == 1) + await redis.KeyExpireAsync(attemptsKey, TimeSpan.FromHours(1)); + } + + return (true, new SendOtpResponse(true, OtpTtlSeconds), null, null); + } + + public async Task<(bool Success, AuthTokenResponse? Data, string? ErrorCode, string? ErrorMessage)> VerifyOtpAsync( + VerifyOtpRequest request, + CancellationToken cancellationToken = default) + { + var phone = PhoneNormalizer.Normalize(request.Phone); + var code = OtpNormalizer.Normalize(request.Code); + if (!OtpNormalizer.IsValidSixDigitCode(code)) + return (false, null, "INVALID_OTP", "Invalid or expired verification code."); + + var redis = _redis.GetDatabase(); + var storedOtp = await redis.StringGetAsync($"otp:admin:{phone}"); + if (storedOtp.IsNullOrEmpty || storedOtp.ToString() != code) + return (false, null, "INVALID_OTP", "Invalid or expired verification code."); + + var admin = await _db.SystemAdmins + .FirstOrDefaultAsync(a => a.Phone == phone && a.IsActive && a.DeletedAt == null, cancellationToken); + if (admin is null) + return (false, null, "NOT_FOUND", "No system admin account for this phone."); + + await redis.KeyDeleteAsync($"otp:admin:{phone}"); + var tokens = await IssueTokensAsync(admin, cancellationToken); + return (true, tokens, null, null); + } + + public async Task<(bool Success, AuthTokenResponse? Data, string? ErrorCode, string? ErrorMessage)> RefreshAsync( + RefreshTokenRequest request, + CancellationToken cancellationToken = default) + { + var payload = await _refreshTokenStore.GetAsync(request.RefreshToken, cancellationToken); + if (payload is null || payload.Actor != MeeziActorKinds.SystemAdmin) + return (false, null, "INVALID_TOKEN", "Refresh token is invalid or expired."); + + var admin = await _db.SystemAdmins + .FirstOrDefaultAsync(a => a.Id == payload.UserId && a.IsActive && a.DeletedAt == null, cancellationToken); + if (admin is null) + return (false, null, "NOT_FOUND", "Admin no longer exists."); + + await _refreshTokenStore.RevokeAsync(request.RefreshToken, cancellationToken); + var tokens = await IssueTokensAsync(admin, cancellationToken); + return (true, tokens, null, null); + } + + private async Task IssueTokensAsync( + Core.Entities.SystemAdmin admin, + CancellationToken cancellationToken) + { + var accessToken = _jwtTokenService.CreateAdminAccessToken(admin); + var refreshToken = _jwtTokenService.CreateRefreshToken(); + var refreshDays = _configuration.GetValue("Jwt:RefreshTokenExpiryDays", 30); + + await _refreshTokenStore.StoreAsync( + refreshToken, + new RefreshTokenPayload( + admin.Id, + string.Empty, + "SystemAdmin", + PlanTier.Enterprise.ToString(), + "fa", + MeeziActorKinds.SystemAdmin), + TimeSpan.FromDays(refreshDays), + cancellationToken); + + return new AuthTokenResponse( + accessToken, + refreshToken, + _jwtTokenService.GetAccessTokenExpiry(), + admin.Id, + string.Empty, + "SystemAdmin", + PlanTier.Enterprise.ToString(), + "fa", + MeeziActorKinds.SystemAdmin); + } +} diff --git a/src/Meezi.Admin.API/Services/AdminJwtTokenService.cs b/src/Meezi.Admin.API/Services/AdminJwtTokenService.cs new file mode 100644 index 0000000..ba6b678 --- /dev/null +++ b/src/Meezi.Admin.API/Services/AdminJwtTokenService.cs @@ -0,0 +1,61 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using Meezi.Core.Constants; +using Meezi.Core.Entities; +using Microsoft.IdentityModel.Tokens; + +namespace Meezi.Admin.API.Services; + +public interface IAdminJwtTokenService +{ + string CreateAdminAccessToken(SystemAdmin admin); + string CreateRefreshToken(); + DateTime GetAccessTokenExpiry(); +} + +public class AdminJwtTokenService : IAdminJwtTokenService +{ + private readonly IConfiguration _configuration; + + public AdminJwtTokenService(IConfiguration configuration) => _configuration = configuration; + + public string CreateAdminAccessToken(SystemAdmin admin) + { + var key = _configuration["Jwt:Key"] ?? throw new InvalidOperationException("Jwt:Key is not configured."); + var issuer = _configuration["Jwt:Issuer"] ?? "meezi"; + var audience = _configuration["Jwt:Audience"] ?? "meezi-admin"; + var expiryDays = _configuration.GetValue("Jwt:AccessTokenExpiryDays", 7); + + var claims = new List + { + new(JwtRegisteredClaimNames.Sub, admin.Id), + new(ClaimTypes.Role, "SystemAdmin"), + new(MeeziClaimTypes.Role, "SystemAdmin"), + new(MeeziClaimTypes.Actor, MeeziActorKinds.SystemAdmin), + new(MeeziClaimTypes.Language, "fa"), + new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString("N")) + }; + + var credentials = new SigningCredentials( + new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key)), + SecurityAlgorithms.HmacSha256); + + var token = new JwtSecurityToken( + issuer, + audience, + claims, + expires: DateTime.UtcNow.AddDays(expiryDays), + signingCredentials: credentials); + + return new JwtSecurityTokenHandler().WriteToken(token); + } + + public string CreateRefreshToken() => Guid.NewGuid().ToString("N") + Guid.NewGuid().ToString("N"); + + public DateTime GetAccessTokenExpiry() + { + var expiryDays = _configuration.GetValue("Jwt:AccessTokenExpiryDays", 7); + return DateTime.UtcNow.AddDays(expiryDays); + } +} diff --git a/src/Meezi.Admin.API/Services/AdminNotificationService.cs b/src/Meezi.Admin.API/Services/AdminNotificationService.cs new file mode 100644 index 0000000..30c405d --- /dev/null +++ b/src/Meezi.Admin.API/Services/AdminNotificationService.cs @@ -0,0 +1,125 @@ +using Microsoft.AspNetCore.SignalR; +using Meezi.Admin.API.Hubs; +using Meezi.Admin.API.Models; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; + +namespace Meezi.Admin.API.Services; + +public interface IAdminNotificationService +{ + Task> ListAsync( + int limit, + string? cafeId, + CancellationToken ct = default); + + Task BroadcastAsync( + string title, + string body, + string adminId, + CancellationToken ct = default); + + Task DeleteAsync(string notificationId, CancellationToken ct = default); +} + +public class AdminNotificationService : IAdminNotificationService +{ + private readonly AppDbContext _db; + private readonly IHubContext _hub; + + public AdminNotificationService(AppDbContext db, IHubContext hub) + { + _db = db; + _hub = hub; + } + + public async Task> ListAsync( + int limit, + string? cafeId, + CancellationToken ct = default) + { + limit = Math.Clamp(limit, 1, 200); + var q = + from n in _db.CafeNotifications.AsNoTracking() + join c in _db.Cafes.AsNoTracking() on n.CafeId equals c.Id + select new { n, c }; + + if (!string.IsNullOrWhiteSpace(cafeId)) + q = q.Where(x => x.n.CafeId == cafeId); + + return await q + .OrderByDescending(x => x.n.CreatedAt) + .Take(limit) + .Select(x => new AdminNotificationRowDto( + x.n.Id, + x.n.CafeId, + x.c.Name, + x.n.Type, + x.n.Title, + x.n.Body, + x.n.IsRead, + x.n.CreatedAt)) + .ToListAsync(ct); + } + + public async Task BroadcastAsync( + string title, + string body, + string adminId, + CancellationToken ct = default) + { + var cafes = await _db.Cafes + .AsNoTracking() + .Where(c => !c.IsSuspended) + .Select(c => c.Id) + .ToListAsync(ct); + + var notifications = new List(); + foreach (var cafeId in cafes) + { + notifications.Add(new Core.Entities.CafeNotification + { + CafeId = cafeId, + Type = "platform_broadcast", + Title = title.Trim(), + Body = body.Trim(), + ReferenceId = adminId + }); + } + + _db.CafeNotifications.AddRange(notifications); + await _db.SaveChangesAsync(ct); + + foreach (var n in notifications) + { + var dto = new CafeNotificationDto( + n.Id, + n.Type, + n.Title, + n.Body, + n.ReferenceId, + n.TableNumber, + n.IsRead, + n.CreatedAt); + + await _hub.Clients.Group(KdsHub.GroupName(n.CafeId)) + .SendAsync("NotificationReceived", dto, ct); + } + + return new BroadcastNotificationResult(cafes.Count, notifications.Count); + } + + public async Task DeleteAsync(string notificationId, CancellationToken ct = default) + { + var row = await _db.CafeNotifications + .IgnoreQueryFilters() + .FirstOrDefaultAsync(n => n.Id == notificationId, ct); + + if (row is null || row.DeletedAt is not null) + return false; + + row.DeletedAt = DateTime.UtcNow; + await _db.SaveChangesAsync(ct); + return true; + } +} diff --git a/src/Meezi.Admin.API/Services/AdminPlatformService.cs b/src/Meezi.Admin.API/Services/AdminPlatformService.cs new file mode 100644 index 0000000..bec1405 --- /dev/null +++ b/src/Meezi.Admin.API/Services/AdminPlatformService.cs @@ -0,0 +1,247 @@ +using System.Text.Json; +using Meezi.Admin.API.Models; +using Meezi.Infrastructure.Services.Platform; +using Meezi.Core.Entities; +using Meezi.Core.Enums; +using Meezi.Core.Interfaces; +using Meezi.Core.Platform; +using Meezi.Core.Discover; +using Meezi.Infrastructure.Data; +using Meezi.Infrastructure.Discover; +using Microsoft.EntityFrameworkCore; + +namespace Meezi.Admin.API.Services; + +public interface IAdminPlatformService +{ + Task GetDashboardStatsAsync(CancellationToken cancellationToken = default); + Task> GetPlansAsync(CancellationToken cancellationToken = default); + Task UpdatePlanAsync(PlanTier tier, UpdatePlanRequest request, CancellationToken cancellationToken = default); + Task> GetSettingsAsync(CancellationToken cancellationToken = default); + Task UpdateSettingAsync(string key, UpdateSettingRequest request, CancellationToken cancellationToken = default); + Task> GetFeaturesAsync(CancellationToken cancellationToken = default); + Task UpdateFeatureAsync(string featureKey, UpdateFeatureRequest request, CancellationToken cancellationToken = default); + Task> ListCafesAsync(CancellationToken cancellationToken = default); + Task PatchCafeAsync(string cafeId, AdminCafePatchRequest request, CancellationToken cancellationToken = default); + Task SetCafeFeatureOverrideAsync(string cafeId, CafeFeatureOverrideRequest request, CancellationToken cancellationToken = default); + Task GetCafeDiscoverProfileAsync(string cafeId, CancellationToken cancellationToken = default); + Task UpsertCafeDiscoverProfileAsync( + string cafeId, + AdminUpsertCafeDiscoverProfileRequest request, + CancellationToken cancellationToken = default); +} + +public class AdminPlatformService : IAdminPlatformService +{ + private static readonly JsonSerializerOptions JsonOpts = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + + private readonly AppDbContext _db; + private readonly IPlatformCatalogService _catalog; + private readonly IPlatformRuntimeConfig _runtime; + + public AdminPlatformService( + AppDbContext db, + IPlatformCatalogService catalog, + IPlatformRuntimeConfig runtime) + { + _db = db; + _catalog = catalog; + _runtime = runtime; + } + + public async Task GetDashboardStatsAsync(CancellationToken cancellationToken = default) + { + var total = await _db.Cafes.CountAsync(cancellationToken); + var suspended = await _db.Cafes.CountAsync(c => c.IsSuspended, cancellationToken); + var openTickets = await _db.SupportTickets.CountAsync( + t => t.Status != SupportTicketStatus.Closed && t.Status != SupportTicketStatus.Resolved, + cancellationToken); + var plans = await _db.PlatformPlanDefinitions.CountAsync(cancellationToken); + + return new AdminDashboardStatsDto( + total, + total - suspended, + suspended, + openTickets, + plans); + } + + public Task> GetPlansAsync(CancellationToken cancellationToken = default) => + _catalog.GetPlansAsync(cancellationToken); + + public async Task UpdatePlanAsync(PlanTier tier, UpdatePlanRequest request, CancellationToken cancellationToken = default) + { + var plan = await _db.PlatformPlanDefinitions.FirstOrDefaultAsync(p => p.Tier == tier, cancellationToken); + if (plan is null) + { + plan = new PlatformPlanDefinition { Tier = tier }; + _db.PlatformPlanDefinitions.Add(plan); + } + + plan.DisplayNameFa = request.DisplayNameFa.Trim(); + plan.DisplayNameEn = request.DisplayNameEn?.Trim(); + plan.MonthlyPriceToman = request.MonthlyPriceToman; + plan.IsBillableOnline = request.IsBillableOnline; + plan.IsActive = request.IsActive; + plan.SortOrder = request.SortOrder; + plan.LimitsJson = JsonSerializer.Serialize(request.Limits, JsonOpts); + plan.FeaturesJson = request.FeatureKeys is null + ? null + : JsonSerializer.Serialize(request.FeatureKeys, JsonOpts); + + await _db.SaveChangesAsync(cancellationToken); + _catalog.InvalidateCache(); + return true; + } + + public Task> GetSettingsAsync(CancellationToken cancellationToken = default) => + _catalog.GetSettingsAsync(cancellationToken); + + public async Task UpdateSettingAsync(string key, UpdateSettingRequest request, CancellationToken cancellationToken = default) + { + var setting = await _db.PlatformSettings.FirstOrDefaultAsync(s => s.Key == key, cancellationToken); + if (setting is null) return false; + + setting.Value = request.Value; + if (request.DescriptionFa is not null) + setting.DescriptionFa = request.DescriptionFa; + setting.UpdatedAt = DateTime.UtcNow; + await _db.SaveChangesAsync(cancellationToken); + _catalog.InvalidateCache(); + _runtime.InvalidateCache(); + return true; + } + + public Task> GetFeaturesAsync(CancellationToken cancellationToken = default) => + _catalog.GetFeaturesAsync(cancellationToken); + + public async Task UpdateFeatureAsync(string featureKey, UpdateFeatureRequest request, CancellationToken cancellationToken = default) + { + var feature = await _db.PlatformFeatures.FirstOrDefaultAsync(f => f.Key == featureKey, cancellationToken); + if (feature is null) return false; + + feature.DisplayNameFa = request.DisplayNameFa.Trim(); + feature.DisplayNameEn = request.DisplayNameEn?.Trim(); + feature.ModuleGroup = request.ModuleGroup.Trim(); + feature.IsEnabledGlobally = request.IsEnabledGlobally; + feature.UpdatedAt = DateTime.UtcNow; + await _db.SaveChangesAsync(cancellationToken); + _catalog.InvalidateCache(); + return true; + } + + public async Task> ListCafesAsync(CancellationToken cancellationToken = default) + { + return await _db.Cafes + .AsNoTracking() + .OrderByDescending(c => c.CreatedAt) + .Select(c => new AdminCafeListItemDto( + c.Id, + c.Name, + c.Slug, + c.City ?? "", + c.PlanTier, + c.PlanExpiresAt, + c.IsSuspended, + c.IsVerified, + c.Branches.Count, + c.Employees.Count(e => e.DeletedAt == null), + c.CreatedAt)) + .ToListAsync(cancellationToken); + } + + public async Task PatchCafeAsync(string cafeId, AdminCafePatchRequest request, CancellationToken cancellationToken = default) + { + var cafe = await _db.Cafes.FirstOrDefaultAsync(c => c.Id == cafeId, cancellationToken); + if (cafe is null) return false; + + if (request.PlanTier.HasValue) + cafe.PlanTier = request.PlanTier.Value; + if (request.PlanExpiresAt.HasValue) + cafe.PlanExpiresAt = request.PlanExpiresAt; + if (request.IsSuspended.HasValue) + cafe.IsSuspended = request.IsSuspended.Value; + if (request.IsVerified.HasValue) + cafe.IsVerified = request.IsVerified.Value; + if (request.DiscoverBadges is not null) + cafe.DiscoverBadgesJson = DiscoverBadgesSerializer.Serialize(request.DiscoverBadges); + + await _db.SaveChangesAsync(cancellationToken); + return true; + } + + public async Task SetCafeFeatureOverrideAsync( + string cafeId, + CafeFeatureOverrideRequest request, + CancellationToken cancellationToken = default) + { + var exists = await _db.Cafes.AnyAsync(c => c.Id == cafeId, cancellationToken); + if (!exists) return false; + + var row = await _db.CafeFeatureOverrides + .FirstOrDefaultAsync(o => o.CafeId == cafeId && o.FeatureKey == request.FeatureKey, cancellationToken); + + if (row is null) + { + row = new CafeFeatureOverride { CafeId = cafeId, FeatureKey = request.FeatureKey }; + _db.CafeFeatureOverrides.Add(row); + } + + row.IsEnabled = request.IsEnabled; + await _db.SaveChangesAsync(cancellationToken); + return true; + } + + public async Task GetCafeDiscoverProfileAsync( + string cafeId, + CancellationToken cancellationToken = default) + { + var cafe = await _db.Cafes.AsNoTracking() + .FirstOrDefaultAsync(c => c.Id == cafeId, cancellationToken); + if (cafe is null) return null; + + var profile = CafeDiscoverProfileSerializer.Deserialize(cafe.DiscoverProfileJson); + return MapAdminDiscoverProfile(cafe.Id, cafe.Name, profile); + } + + public async Task UpsertCafeDiscoverProfileAsync( + string cafeId, + AdminUpsertCafeDiscoverProfileRequest request, + CancellationToken cancellationToken = default) + { + var cafe = await _db.Cafes.FirstOrDefaultAsync(c => c.Id == cafeId, cancellationToken); + if (cafe is null) return null; + + var profile = CafeDiscoverProfileSerializer.Sanitize(new CafeDiscoverProfile + { + Themes = request.Themes?.ToList() ?? [], + Size = request.Size, + Floors = request.Floors, + Vibes = request.Vibes?.ToList() ?? [], + Occasions = request.Occasions?.ToList() ?? [], + SpaceFeatures = request.SpaceFeatures?.ToList() ?? [], + NoiseLevel = request.NoiseLevel, + PriceTier = request.PriceTier + }); + + cafe.DiscoverProfileJson = CafeDiscoverProfileSerializer.Serialize(profile); + await _db.SaveChangesAsync(cancellationToken); + return MapAdminDiscoverProfile(cafe.Id, cafe.Name, profile); + } + + private static AdminCafeDiscoverProfileDto MapAdminDiscoverProfile( + string cafeId, + string cafeName, + CafeDiscoverProfile profile) => + new( + cafeId, + cafeName, + profile.Themes, + profile.Size, + profile.Floors, + profile.Vibes, + profile.Occasions, + profile.SpaceFeatures, + profile.NoiseLevel, + profile.PriceTier); +} diff --git a/src/Meezi.Admin.API/Services/AdminWebsiteService.cs b/src/Meezi.Admin.API/Services/AdminWebsiteService.cs new file mode 100644 index 0000000..61ae1e3 --- /dev/null +++ b/src/Meezi.Admin.API/Services/AdminWebsiteService.cs @@ -0,0 +1,166 @@ +using Meezi.Admin.API.Controllers; +using Meezi.Core.Entities; +using Meezi.Core.Enums; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; + +namespace Meezi.Admin.API.Services; + +public class AdminWebsiteService(AppDbContext db) : IAdminWebsiteService +{ + // ── Posts ───────────────────────────────────────────────────────────── + + public async Task ListPostsAsync(int page, int limit, bool? published, CancellationToken ct) + { + var q = db.WebsiteBlogPosts.AsQueryable(); + if (published.HasValue) q = q.Where(p => p.IsPublished == published.Value); + var total = await q.CountAsync(ct); + var posts = await q.OrderByDescending(p => p.CreatedAt) + .Skip((page - 1) * limit).Take(limit).ToListAsync(ct); + return new { Posts = posts.Select(MapPost), Total = total, Page = page, Limit = limit }; + } + + public async Task GetPostAsync(string id, CancellationToken ct) + { + var post = await db.WebsiteBlogPosts.FindAsync([id], ct); + return post is null ? null : MapPost(post); + } + + public async Task CreatePostAsync(UpsertPostRequest req, CancellationToken ct) + { + var post = new WebsiteBlogPost + { + Slug = req.Slug.Trim().ToLowerInvariant(), + TitleFa = req.TitleFa, + TitleEn = req.TitleEn ?? "", + ExcerptFa = req.ExcerptFa ?? "", + ExcerptEn = req.ExcerptEn ?? "", + ContentFa = req.ContentFa, + ContentEn = req.ContentEn ?? "", + CategoryFa = req.CategoryFa ?? "", + CategoryEn = req.CategoryEn ?? "", + Author = req.Author ?? "تیم میزی", + TagsJson = req.TagsJson ?? "[]", + CoverImage = req.CoverImage, + IsPublished = req.IsPublished, + PublishedAt = req.IsPublished ? DateTime.UtcNow : null, + }; + db.WebsiteBlogPosts.Add(post); + await db.SaveChangesAsync(ct); + return MapPost(post); + } + + public async Task UpdatePostAsync(string id, UpsertPostRequest req, CancellationToken ct) + { + var post = await db.WebsiteBlogPosts.FindAsync([id], ct); + if (post is null) return null; + + post.Slug = req.Slug.Trim().ToLowerInvariant(); + post.TitleFa = req.TitleFa; + post.TitleEn = req.TitleEn ?? ""; + post.ExcerptFa = req.ExcerptFa ?? ""; + post.ExcerptEn = req.ExcerptEn ?? ""; + post.ContentFa = req.ContentFa; + post.ContentEn = req.ContentEn ?? ""; + post.CategoryFa = req.CategoryFa ?? ""; + post.CategoryEn = req.CategoryEn ?? ""; + post.Author = req.Author ?? post.Author; + post.TagsJson = req.TagsJson ?? "[]"; + post.CoverImage = req.CoverImage; + if (req.IsPublished && !post.IsPublished) post.PublishedAt = DateTime.UtcNow; + post.IsPublished = req.IsPublished; + + await db.SaveChangesAsync(ct); + return MapPost(post); + } + + public async Task DeletePostAsync(string id, CancellationToken ct) + { + var post = await db.WebsiteBlogPosts.FindAsync([id], ct); + if (post is not null) { post.DeletedAt = DateTime.UtcNow; await db.SaveChangesAsync(ct); } + } + + public async Task SetPublishedAsync(string id, bool published, CancellationToken ct) + { + var post = await db.WebsiteBlogPosts.FindAsync([id], ct); + if (post is null) return; + post.IsPublished = published; + if (published && post.PublishedAt is null) post.PublishedAt = DateTime.UtcNow; + await db.SaveChangesAsync(ct); + } + + // ── Comments ────────────────────────────────────────────────────────── + + public async Task ListCommentsAsync(bool? approved, int page, int limit, CancellationToken ct) + { + var q = db.WebsiteComments.AsQueryable(); + if (approved.HasValue) q = q.Where(c => c.IsApproved == approved.Value); + var total = await q.CountAsync(ct); + var comments = await q.OrderByDescending(c => c.CreatedAt) + .Skip((page - 1) * limit).Take(limit).ToListAsync(ct); + return new + { + Comments = comments.Select(c => new + { + c.Id, c.PostSlug, c.AuthorName, c.AuthorEmail, + c.Content, c.IsApproved, c.CreatedAt, c.IpAddress, + }), + Total = total, Page = page, Limit = limit, + }; + } + + public async Task SetCommentApprovedAsync(string id, bool approved, CancellationToken ct) + { + var c = await db.WebsiteComments.FindAsync([id], ct); + if (c is null) return; + c.IsApproved = approved; + await db.SaveChangesAsync(ct); + } + + public async Task DeleteCommentAsync(string id, CancellationToken ct) + { + var c = await db.WebsiteComments.FindAsync([id], ct); + if (c is not null) { c.DeletedAt = DateTime.UtcNow; await db.SaveChangesAsync(ct); } + } + + // ── Demo requests ───────────────────────────────────────────────────── + + public async Task ListDemoRequestsAsync(string? status, int page, int limit, CancellationToken ct) + { + var q = db.DemoRequests.AsQueryable(); + if (status is not null && Enum.TryParse(status, true, out var s)) + q = q.Where(r => r.Status == s); + var total = await q.CountAsync(ct); + var reqs = await q.OrderByDescending(r => r.CreatedAt) + .Skip((page - 1) * limit).Take(limit).ToListAsync(ct); + return new + { + Requests = reqs.Select(r => new + { + r.Id, r.ContactName, r.BusinessName, r.Phone, r.Email, + r.BranchCount, r.Notes, r.Source, r.AdminNotes, + Status = r.Status.ToString(), r.ContactedAt, r.CreatedAt, + }), + Total = total, Page = page, Limit = limit, + }; + } + + public async Task UpdateDemoStatusAsync(string id, string status, string? adminNotes, CancellationToken ct) + { + var req = await db.DemoRequests.FindAsync([id], ct); + if (req is null) return; + if (Enum.TryParse(status, true, out var s)) req.Status = s; + if (adminNotes is not null) req.AdminNotes = adminNotes; + if (s == DemoRequestStatus.Contacted && req.ContactedAt is null) req.ContactedAt = DateTime.UtcNow; + await db.SaveChangesAsync(ct); + } + + // ── Mapper ──────────────────────────────────────────────────────────── + + private static object MapPost(WebsiteBlogPost p) => new + { + p.Id, p.Slug, p.TitleFa, p.TitleEn, p.ExcerptFa, p.ExcerptEn, + p.ContentFa, p.ContentEn, p.CategoryFa, p.CategoryEn, p.Author, + p.TagsJson, p.CoverImage, p.IsPublished, p.PublishedAt, p.ViewCount, p.CreatedAt, + }; +} diff --git a/src/Meezi.Admin.API/Services/IAdminWebsiteService.cs b/src/Meezi.Admin.API/Services/IAdminWebsiteService.cs new file mode 100644 index 0000000..f5a2b08 --- /dev/null +++ b/src/Meezi.Admin.API/Services/IAdminWebsiteService.cs @@ -0,0 +1,20 @@ +using Meezi.Admin.API.Controllers; + +namespace Meezi.Admin.API.Services; + +public interface IAdminWebsiteService +{ + Task ListPostsAsync(int page, int limit, bool? published, CancellationToken ct); + Task GetPostAsync(string id, CancellationToken ct); + Task CreatePostAsync(UpsertPostRequest req, CancellationToken ct); + Task UpdatePostAsync(string id, UpsertPostRequest req, CancellationToken ct); + Task DeletePostAsync(string id, CancellationToken ct); + Task SetPublishedAsync(string id, bool published, CancellationToken ct); + + Task ListCommentsAsync(bool? approved, int page, int limit, CancellationToken ct); + Task SetCommentApprovedAsync(string id, bool approved, CancellationToken ct); + Task DeleteCommentAsync(string id, CancellationToken ct); + + Task ListDemoRequestsAsync(string? status, int page, int limit, CancellationToken ct); + Task UpdateDemoStatusAsync(string id, string status, string? adminNotes, CancellationToken ct); +} diff --git a/src/Meezi.Admin.API/Services/PlatformIntegrationService.cs b/src/Meezi.Admin.API/Services/PlatformIntegrationService.cs new file mode 100644 index 0000000..b156d5b --- /dev/null +++ b/src/Meezi.Admin.API/Services/PlatformIntegrationService.cs @@ -0,0 +1,255 @@ +using Meezi.Admin.API.Models; +using Meezi.Core.Platform; +using Meezi.Infrastructure.Services.Platform; +using Meezi.Core.Interfaces; +using Meezi.Core.Entities; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; + +namespace Meezi.Admin.API.Services; + +public interface IPlatformIntegrationService +{ + Task GetIntegrationsAsync(CancellationToken ct = default); + Task SaveIntegrationsAsync(UpdatePlatformIntegrationsRequest request, CancellationToken ct = default); +} + +public class PlatformIntegrationService : IPlatformIntegrationService +{ + public const string KeyActiveGateway = "payment.activeGateway"; + public const string KeyKavenegarApi = "integrations.kavenegar.apiKey"; + public const string KeyKavenegarOtpTemplate = "integrations.kavenegar.otpTemplate"; + public const string KeyKavenegarEnabled = "integrations.kavenegar.enabled"; + + private static readonly (string Id, string NameFa, string Prefix)[] Gateways = + [ + ("zarinpal", "زرین‌پال", "payment.zarinpal"), + ("tara", "تارا", "payment.tara"), + ("snapppay", "اسنپ‌پی", "payment.snapppay"), + ("nextpay", "نکست‌پی", "payment.nextpay"), + ("vandar", "وندار", "payment.vandar") + ]; + + private readonly AppDbContext _db; + private readonly IPlatformCatalogService _catalog; + private readonly IPlatformRuntimeConfig _runtime; + + public PlatformIntegrationService( + AppDbContext db, + IPlatformCatalogService catalog, + IPlatformRuntimeConfig runtime) + { + _db = db; + _catalog = catalog; + _runtime = runtime; + } + + public async Task GetIntegrationsAsync(CancellationToken ct = default) + { + var settings = await _db.PlatformSettings.AsNoTracking().ToListAsync(ct); + var map = settings.ToDictionary(s => s.Key, s => s.Value, StringComparer.OrdinalIgnoreCase); + + var active = map.GetValueOrDefault(KeyActiveGateway) ?? "zarinpal"; + var gateways = Gateways.Select(g => MapGateway(g.Id, g.NameFa, g.Prefix, active, map)).ToList(); + + var kavenegar = new KavenegarConfigDto( + map.GetValueOrDefault(KeyKavenegarEnabled) is "true", + MaskSecret(map.GetValueOrDefault(KeyKavenegarApi)), + map.GetValueOrDefault(KeyKavenegarOtpTemplate) ?? "verify", + HasSecret(map, KeyKavenegarApi)); + + var ai = new AiIntegrationsConfigDto( + new OpenAiIntegrationConfigDto( + map.GetValueOrDefault(PlatformIntegrationKeys.OpenAiEnabled) is not "false", + MaskSecret(map.GetValueOrDefault(PlatformIntegrationKeys.OpenAiApiKey)), + map.GetValueOrDefault(PlatformIntegrationKeys.OpenAiModel) ?? "gpt-4o-mini", + map.GetValueOrDefault(PlatformIntegrationKeys.OpenAiCoffeeAdvisorEnabled) is not "false", + HasSecret(map, PlatformIntegrationKeys.OpenAiApiKey)), + new MeshyIntegrationConfigDto( + map.GetValueOrDefault(PlatformIntegrationKeys.MeshyEnabled) is not "false", + MaskSecret(map.GetValueOrDefault(PlatformIntegrationKeys.MeshyApiKey)), + map.GetValueOrDefault(PlatformIntegrationKeys.MeshyMenu3dEnabled) is not "false", + HasSecret(map, PlatformIntegrationKeys.MeshyApiKey))); + + return new PlatformIntegrationsDto(active, gateways, kavenegar, ai); + } + + public async Task SaveIntegrationsAsync(UpdatePlatformIntegrationsRequest request, CancellationToken ct = default) + { + var active = request.ActivePaymentGateway.Trim().ToLowerInvariant(); + if (!Gateways.Any(g => g.Id == active)) + active = "zarinpal"; + + await UpsertAsync(KeyActiveGateway, active, "payment", "درگاه پیش‌فرض اشتراک", ct); + + foreach (var gw in request.PaymentGateways) + { + var meta = Gateways.FirstOrDefault(g => g.Id == gw.Id); + if (string.IsNullOrEmpty(meta.Id)) continue; + + await UpsertAsync($"{meta.Prefix}.enabled", gw.IsEnabled ? "true" : "false", "payment", $"فعال {meta.NameFa}", ct); + await UpsertAsync($"{meta.Prefix}.sandbox", gw.Sandbox ? "true" : "false", "payment", $"حالت تست {meta.NameFa}", ct); + + if (gw.Id == "zarinpal") + { + if (!string.IsNullOrWhiteSpace(gw.MerchantId)) + await UpsertAsync($"{meta.Prefix}.merchantId", gw.MerchantId.Trim(), "payment", "مرچنت زرین‌پال", ct); + } + else if (gw.Id is "nextpay" or "vandar") + { + if (!string.IsNullOrWhiteSpace(gw.ApiKey) && !IsMaskedPlaceholder(gw.ApiKey)) + await UpsertAsync($"{meta.Prefix}.apiKey", gw.ApiKey.Trim(), "payment", $"توکن {meta.NameFa}", ct); + } + + if (gw.Credentials is not null) + await SaveCredentialsAsync(meta.Prefix, gw.Id, gw.Credentials, ct); + } + + await UpsertAsync(KeyKavenegarEnabled, request.Kavenegar.IsEnabled ? "true" : "false", "integrations", "فعال کاوه‌نگار", ct); + await UpsertAsync(KeyKavenegarOtpTemplate, request.Kavenegar.OtpTemplate.Trim(), "integrations", "قالب OTP", ct); + if (!string.IsNullOrWhiteSpace(request.Kavenegar.ApiKey) && !IsMaskedPlaceholder(request.Kavenegar.ApiKey)) + await UpsertAsync(KeyKavenegarApi, request.Kavenegar.ApiKey.Trim(), "integrations", "API Key کاوه‌نگار", ct); + + await UpsertAsync(PlatformIntegrationKeys.OpenAiEnabled, request.Ai.OpenAi.IsEnabled ? "true" : "false", "integrations", "فعال OpenAI", ct); + await UpsertAsync(PlatformIntegrationKeys.OpenAiModel, string.IsNullOrWhiteSpace(request.Ai.OpenAi.Model) ? "gpt-4o-mini" : request.Ai.OpenAi.Model.Trim(), "integrations", "مدل OpenAI", ct); + await UpsertAsync(PlatformIntegrationKeys.OpenAiCoffeeAdvisorEnabled, request.Ai.OpenAi.CoffeeAdvisorEnabled ? "true" : "false", "integrations", "مشاور قهوه OpenAI", ct); + if (!string.IsNullOrWhiteSpace(request.Ai.OpenAi.ApiKey) && !IsMaskedPlaceholder(request.Ai.OpenAi.ApiKey)) + await UpsertAsync(PlatformIntegrationKeys.OpenAiApiKey, request.Ai.OpenAi.ApiKey.Trim(), "integrations", "API Key OpenAI", ct); + + await UpsertAsync(PlatformIntegrationKeys.MeshyEnabled, request.Ai.Meshy.IsEnabled ? "true" : "false", "integrations", "فعال Meshy", ct); + await UpsertAsync(PlatformIntegrationKeys.MeshyMenu3dEnabled, request.Ai.Meshy.Menu3dEnabled ? "true" : "false", "integrations", "ساخت ۳D منو با Meshy", ct); + if (!string.IsNullOrWhiteSpace(request.Ai.Meshy.ApiKey) && !IsMaskedPlaceholder(request.Ai.Meshy.ApiKey)) + await UpsertAsync(PlatformIntegrationKeys.MeshyApiKey, request.Ai.Meshy.ApiKey.Trim(), "integrations", "API Key Meshy", ct); + + await _db.SaveChangesAsync(ct); + _catalog.InvalidateCache(); + _runtime.InvalidateCache(); + } + + private async Task SaveCredentialsAsync( + string prefix, + string gatewayId, + UpdatePaymentGatewayCredentialsRequest creds, + CancellationToken ct) + { + if (!string.IsNullOrWhiteSpace(creds.BaseUrl)) + await UpsertAsync($"{prefix}.baseUrl", creds.BaseUrl.Trim(), "payment", "آدرس API", ct); + + if (gatewayId == "tara") + { + if (!string.IsNullOrWhiteSpace(creds.Username)) + await UpsertAsync($"{prefix}.username", creds.Username.Trim(), "payment", "نام کاربری تارا", ct); + if (!string.IsNullOrWhiteSpace(creds.Password) && !IsMaskedPlaceholder(creds.Password)) + await UpsertAsync($"{prefix}.password", creds.Password.Trim(), "payment", "رمز تارا", ct); + if (!string.IsNullOrWhiteSpace(creds.BranchCode)) + await UpsertAsync($"{prefix}.branchCode", creds.BranchCode.Trim(), "payment", "کد شعبه تارا", ct); + if (!string.IsNullOrWhiteSpace(creds.TerminalCode)) + await UpsertAsync($"{prefix}.terminalCode", creds.TerminalCode.Trim(), "payment", "ترمینال تارا", ct); + } + else if (gatewayId == "snapppay") + { + if (!string.IsNullOrWhiteSpace(creds.ClientId)) + await UpsertAsync($"{prefix}.clientId", creds.ClientId.Trim(), "payment", "Client ID اسنپ‌پی", ct); + if (!string.IsNullOrWhiteSpace(creds.ClientSecret) && !IsMaskedPlaceholder(creds.ClientSecret)) + await UpsertAsync($"{prefix}.clientSecret", creds.ClientSecret.Trim(), "payment", "Client Secret اسنپ‌پی", ct); + if (!string.IsNullOrWhiteSpace(creds.Username)) + await UpsertAsync($"{prefix}.username", creds.Username.Trim(), "payment", "نام کاربری اسنپ‌پی", ct); + if (!string.IsNullOrWhiteSpace(creds.Password) && !IsMaskedPlaceholder(creds.Password)) + await UpsertAsync($"{prefix}.password", creds.Password.Trim(), "payment", "رمز اسنپ‌پی", ct); + } + } + + private async Task UpsertAsync(string key, string value, string category, string descFa, CancellationToken ct) + { + var row = await _db.PlatformSettings.FirstOrDefaultAsync(s => s.Key == key, ct); + if (row is null) + { + _db.PlatformSettings.Add(new PlatformSetting + { + Key = key, + Value = value, + Category = category, + DescriptionFa = descFa + }); + } + else + { + row.Value = value; + row.UpdatedAt = DateTime.UtcNow; + } + } + + private static PaymentGatewayConfigDto MapGateway( + string id, + string nameFa, + string prefix, + string activeGateway, + Dictionary map) + { + var enabled = map.GetValueOrDefault($"{prefix}.enabled") is "true"; + var sandbox = map.GetValueOrDefault($"{prefix}.sandbox") is not "false"; + string? merchantId = null; + string? apiKey = null; + var hasSecret = false; + GatewayCredentialsDto? credentials = null; + + if (id == "zarinpal") + { + merchantId = map.GetValueOrDefault($"{prefix}.merchantId"); + hasSecret = HasSecret(map, $"{prefix}.merchantId"); + } + else if (id is "nextpay" or "vandar") + { + apiKey = MaskSecret(map.GetValueOrDefault($"{prefix}.apiKey")); + hasSecret = HasSecret(map, $"{prefix}.apiKey"); + } + else if (id == "tara") + { + credentials = new GatewayCredentialsDto( + map.GetValueOrDefault($"{prefix}.username"), + MaskSecret(map.GetValueOrDefault($"{prefix}.password")), + map.GetValueOrDefault($"{prefix}.branchCode"), + map.GetValueOrDefault($"{prefix}.terminalCode"), + null, + null, + map.GetValueOrDefault($"{prefix}.baseUrl"), + HasSecret(map, $"{prefix}.password"), + false); + hasSecret = credentials.HasStoredPassword; + } + else if (id == "snapppay") + { + credentials = new GatewayCredentialsDto( + map.GetValueOrDefault($"{prefix}.username"), + MaskSecret(map.GetValueOrDefault($"{prefix}.password")), + null, + null, + map.GetValueOrDefault($"{prefix}.clientId"), + MaskSecret(map.GetValueOrDefault($"{prefix}.clientSecret")), + map.GetValueOrDefault($"{prefix}.baseUrl"), + HasSecret(map, $"{prefix}.password"), + HasSecret(map, $"{prefix}.clientSecret")); + hasSecret = credentials.HasStoredPassword || credentials.HasStoredClientSecret; + } + + return new PaymentGatewayConfigDto( + id, + nameFa, + enabled, + activeGateway == id, + merchantId, + apiKey, + sandbox, + hasSecret, + credentials); + } + + private static bool HasSecret(Dictionary map, string key) => + !string.IsNullOrWhiteSpace(map.GetValueOrDefault(key)); + + private static string? MaskSecret(string? value) => + string.IsNullOrWhiteSpace(value) ? null : "••••••••"; + + private static bool IsMaskedPlaceholder(string? value) => + string.IsNullOrWhiteSpace(value) || value.Contains("••••", StringComparison.Ordinal); +} diff --git a/src/Meezi.Admin.API/Services/RefreshTokenStore.cs b/src/Meezi.Admin.API/Services/RefreshTokenStore.cs new file mode 100644 index 0000000..8e3f108 --- /dev/null +++ b/src/Meezi.Admin.API/Services/RefreshTokenStore.cs @@ -0,0 +1,48 @@ +using System.Text.Json; +using StackExchange.Redis; + +namespace Meezi.Admin.API.Services; + +public record RefreshTokenPayload( + string UserId, + string CafeId, + string Role, + string PlanTier, + string Language, + string Actor = Meezi.Core.Constants.MeeziActorKinds.SystemAdmin); + +public interface IRefreshTokenStore +{ + Task StoreAsync(string refreshToken, RefreshTokenPayload payload, TimeSpan ttl, CancellationToken cancellationToken = default); + Task GetAsync(string refreshToken, CancellationToken cancellationToken = default); + Task RevokeAsync(string refreshToken, CancellationToken cancellationToken = default); +} + +public class RedisRefreshTokenStore : IRefreshTokenStore +{ + private readonly IConnectionMultiplexer _redis; + + public RedisRefreshTokenStore(IConnectionMultiplexer redis) => _redis = redis; + + private static string Key(string token) => $"admin:refresh:{token}"; + + public async Task StoreAsync(string refreshToken, RefreshTokenPayload payload, TimeSpan ttl, CancellationToken cancellationToken = default) + { + var db = _redis.GetDatabase(); + await db.StringSetAsync(Key(refreshToken), JsonSerializer.Serialize(payload), ttl); + } + + public async Task GetAsync(string refreshToken, CancellationToken cancellationToken = default) + { + var db = _redis.GetDatabase(); + var value = await db.StringGetAsync(Key(refreshToken)); + if (value.IsNullOrEmpty) return null; + return JsonSerializer.Deserialize(value.ToString()); + } + + public async Task RevokeAsync(string refreshToken, CancellationToken cancellationToken = default) + { + var db = _redis.GetDatabase(); + await db.KeyDeleteAsync(Key(refreshToken)); + } +} diff --git a/src/Meezi.Admin.API/Validators/AuthValidators.cs b/src/Meezi.Admin.API/Validators/AuthValidators.cs new file mode 100644 index 0000000..850cd62 --- /dev/null +++ b/src/Meezi.Admin.API/Validators/AuthValidators.cs @@ -0,0 +1,24 @@ +using FluentValidation; +using Meezi.Admin.API.Models; +using Meezi.Core.Utilities; + +namespace Meezi.Admin.API.Validators; + +public class SendOtpRequestValidator : AbstractValidator +{ + public SendOtpRequestValidator() + { + RuleFor(x => x.Phone).Must(PhoneNormalizer.IsValidIranMobile).WithMessage("Invalid phone number."); + } +} + +public class VerifyOtpRequestValidator : AbstractValidator +{ + public VerifyOtpRequestValidator() + { + RuleFor(x => x.Phone).Must(PhoneNormalizer.IsValidIranMobile); + RuleFor(x => x.Code) + .Must(OtpNormalizer.IsValidSixDigitCode) + .WithMessage("OTP must be 6 digits."); + } +} diff --git a/src/Meezi.Admin.API/appsettings.json b/src/Meezi.Admin.API/appsettings.json new file mode 100644 index 0000000..9d22704 --- /dev/null +++ b/src/Meezi.Admin.API/appsettings.json @@ -0,0 +1,32 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "DefaultConnection": "Host=localhost;Port=5434;Database=meezi;Username=meezi;Password=meezi_local_pass", + "Redis": "localhost:6381" + }, + "Jwt": { + "Key": "meezi-dev-secret-key-min-32-chars!!", + "Issuer": "meezi", + "Audience": "meezi-admin", + "AccessTokenExpiryDays": 7, + "RefreshTokenExpiryDays": 30 + }, + "Cors": { + "Origins": [ + "http://localhost:3000", + "http://localhost:3101", + "http://localhost:3102", + "https://localhost:3000" + ] + }, + "Kavenegar": { + "ApiKey": "", + "OtpTemplate": "verify" + } +} diff --git a/src/Meezi.Core/Branding/CafeTheme.cs b/src/Meezi.Core/Branding/CafeTheme.cs new file mode 100644 index 0000000..b9ea009 --- /dev/null +++ b/src/Meezi.Core/Branding/CafeTheme.cs @@ -0,0 +1,78 @@ +namespace Meezi.Core.Branding; + +/// Per-café dashboard + public menu appearance (stored as JSON on ). +public class CafeTheme +{ + public string PaletteId { get; set; } = CafeThemeDefaults.PaletteMeeziGreen; + public string PanelStyle { get; set; } = CafeThemeDefaults.PanelModern; + public string MenuStyle { get; set; } = CafeThemeDefaults.MenuCards; + /// Background texture for QR / guest table menu. + public string MenuTexture { get; set; } = CafeThemeDefaults.MenuTextureNone; + public string Density { get; set; } = CafeThemeDefaults.DensityComfortable; + public string Radius { get; set; } = CafeThemeDefaults.RadiusMd; + public CafeThemeCustomColors? Custom { get; set; } +} + +public class CafeThemeCustomColors +{ + public string? Primary { get; set; } + public string? Secondary { get; set; } + public string? Accent { get; set; } + public string? Background { get; set; } + public string? Surface { get; set; } + public string? Text { get; set; } + public string? TextMuted { get; set; } + public string? Destructive { get; set; } + public string? Success { get; set; } + + /// 0–100 opacity for custom primary (when set). + public int? PrimaryOpacity { get; set; } + public int? SecondaryOpacity { get; set; } + public int? AccentOpacity { get; set; } + public int? BackgroundOpacity { get; set; } + public int? SurfaceOpacity { get; set; } + public int? TextOpacity { get; set; } + public int? TextMutedOpacity { get; set; } + public int? DestructiveOpacity { get; set; } + public int? SuccessOpacity { get; set; } +} + +public static class CafeThemeDefaults +{ + public const string PaletteMeeziGreen = "meezi-green"; + + public const string PanelFlat = "flat"; + public const string PanelModern = "modern"; + public const string PanelGlass = "glass"; + public const string PanelMinimal = "minimal"; + public const string PanelBold = "bold"; + public const string PanelSoft = "soft"; + public const string PanelElevated = "elevated"; + public const string PanelOutline = "outline"; + + public const string MenuCards = "cards"; + public const string MenuCompact = "compact"; + public const string MenuGrid = "grid"; + public const string MenuList = "list"; + public const string MenuMagazine = "magazine"; + public const string MenuClassic = "classic"; + + public const string MenuTextureNone = "none"; + public const string MenuTexturePaper = "paper"; + public const string MenuTextureLinen = "linen"; + public const string MenuTextureDots = "dots"; + public const string MenuTextureGrid = "grid"; + public const string MenuTextureMarble = "marble"; + public const string MenuTextureWood = "wood"; + public const string MenuTextureWarm = "warm"; + + public const string DensityCompact = "compact"; + public const string DensityComfortable = "comfortable"; + public const string DensitySpacious = "spacious"; + + public const string RadiusNone = "none"; + public const string RadiusSm = "sm"; + public const string RadiusMd = "md"; + public const string RadiusLg = "lg"; + public const string RadiusFull = "full"; +} diff --git a/src/Meezi.Core/Constants/CategoryIconPresets.cs b/src/Meezi.Core/Constants/CategoryIconPresets.cs new file mode 100644 index 0000000..03b2878 --- /dev/null +++ b/src/Meezi.Core/Constants/CategoryIconPresets.cs @@ -0,0 +1,73 @@ +namespace Meezi.Core.Constants; + +/// Preset food/drink category icons (dashboard + POS). is the visual variant. +public static class CategoryIconPresets +{ + public static class IconStyle + { + public const string Flat = "flat"; + public const string Modern = "modern"; + public const string Real = "real"; + public const string Minimal = "minimal"; + public const string Outline = "outline"; + public const string Soft = "soft"; + public const string Bold = "bold"; + public const string Gradient = "gradient"; + public const string Pastel = "pastel"; + public const string Duotone = "duotone"; + + public static readonly IReadOnlySet All = new HashSet(StringComparer.Ordinal) + { + Flat, Modern, Real, Minimal, Outline, Soft, Bold, Gradient, Pastel, Duotone + }; + } + + public static class PresetId + { + public const string DrinksHot = "drinks-hot"; + public const string DrinksCold = "drinks-cold"; + public const string DrinksTea = "drinks-tea"; + public const string DrinksJuice = "drinks-juice"; + public const string DrinksMilkshake = "drinks-milkshake"; + public const string DrinksAlcohol = "drinks-alcohol"; + public const string DrinksBeer = "drinks-beer"; + public const string Breakfast = "breakfast"; + public const string FoodMains = "food-mains"; + public const string FoodFastfood = "food-fastfood"; + public const string FoodRice = "food-rice"; + public const string PastaPizza = "pasta-pizza"; + public const string Dessert = "dessert"; + public const string IceCream = "ice-cream"; + public const string Bakery = "bakery"; + public const string Salad = "salad"; + public const string Grill = "grill"; + public const string Seafood = "seafood"; + public const string Snacks = "snacks"; + public const string SnacksSweet = "snacks-sweet"; + public const string Appetizers = "appetizers"; + public const string Vegan = "vegan"; + public const string Fruits = "fruits"; + public const string Specials = "specials"; + public const string ChefSpecial = "chef-special"; + public const string Generic = "generic"; + + public static readonly IReadOnlySet All = new HashSet(StringComparer.Ordinal) + { + DrinksHot, DrinksCold, DrinksTea, DrinksJuice, DrinksMilkshake, DrinksAlcohol, DrinksBeer, + Breakfast, FoodMains, FoodFastfood, FoodRice, PastaPizza, Dessert, IceCream, Bakery, Salad, + Grill, Seafood, Snacks, SnacksSweet, Appetizers, Vegan, Fruits, Specials, ChefSpecial, Generic + }; + } + + public static bool IsValidStyle(string? style) => + !string.IsNullOrWhiteSpace(style) && IconStyle.All.Contains(style.Trim()); + + public static bool IsValidPreset(string? presetId) => + !string.IsNullOrWhiteSpace(presetId) && PresetId.All.Contains(presetId.Trim()); + + public static string? NormalizeStyle(string? style) => + string.IsNullOrWhiteSpace(style) ? null : style.Trim().ToLowerInvariant(); + + public static string? NormalizePreset(string? presetId) => + string.IsNullOrWhiteSpace(presetId) ? null : presetId.Trim().ToLowerInvariant(); +} diff --git a/src/Meezi.Core/Constants/ClaimTypes.cs b/src/Meezi.Core/Constants/ClaimTypes.cs new file mode 100644 index 0000000..68a62cb --- /dev/null +++ b/src/Meezi.Core/Constants/ClaimTypes.cs @@ -0,0 +1,24 @@ +namespace Meezi.Core.Constants; + +public static class MeeziClaimTypes +{ + public const string CafeId = "cafeId"; + public const string Role = "role"; + public const string PlanTier = "planTier"; + public const string Language = "lang"; + public const string BranchId = "branchId"; + public const string Actor = "actor"; + public const string Phone = "phone"; +} + +public static class MeeziActorKinds +{ + public const string Merchant = "merchant"; + public const string SystemAdmin = "systemAdmin"; + public const string Consumer = "consumer"; +} + +public static class MeeziRoles +{ + public const string Customer = "customer"; +} diff --git a/src/Meezi.Core/Constants/PlanLimits.cs b/src/Meezi.Core/Constants/PlanLimits.cs new file mode 100644 index 0000000..c160c26 --- /dev/null +++ b/src/Meezi.Core/Constants/PlanLimits.cs @@ -0,0 +1,57 @@ +using Meezi.Core.Enums; + +namespace Meezi.Core.Constants; + +public static class PlanLimits +{ + public static int MaxOrdersPerDay(PlanTier tier) => tier switch + { + PlanTier.Free => 50, + _ => int.MaxValue + }; + + public static int MaxTerminals(PlanTier tier) => tier switch + { + PlanTier.Free => 1, + PlanTier.Pro => 3, + _ => int.MaxValue + }; + + public static int MaxCustomers(PlanTier tier) => tier switch + { + PlanTier.Free => 50, + _ => int.MaxValue + }; + + public static int MaxSmsPerMonth(PlanTier tier) => tier switch + { + PlanTier.Free => 0, + PlanTier.Pro => 50, + PlanTier.Business => 200, + _ => int.MaxValue + }; + + public static int MaxBranches(PlanTier tier) => tier switch + { + PlanTier.Free => 1, + PlanTier.Pro => 3, + PlanTier.Business => int.MaxValue, + _ => int.MaxValue + }; + + /// How far back daily report snapshots may be queried (inclusive of today). + public static int MaxReportHistoryDays(PlanTier tier) => tier switch + { + PlanTier.Free => 8, + PlanTier.Pro => 90, + _ => int.MaxValue + }; + + /// AI image-to-3D generations per calendar month (UTC). + public static int MaxMenuAi3dPerMonth(PlanTier tier) => tier switch + { + PlanTier.Business => 100, + PlanTier.Enterprise => 100, + _ => 0 + }; +} diff --git a/src/Meezi.Core/Constants/PlanPricing.cs b/src/Meezi.Core/Constants/PlanPricing.cs new file mode 100644 index 0000000..b7bd347 --- /dev/null +++ b/src/Meezi.Core/Constants/PlanPricing.cs @@ -0,0 +1,20 @@ +using Meezi.Core.Enums; + +namespace Meezi.Core.Constants; + +public static class PlanPricing +{ + /// Monthly price in Toman (ت). + public static decimal MonthlyToman(PlanTier tier) => tier switch + { + PlanTier.Pro => 1_490_000m, + PlanTier.Business => 3_490_000m, + _ => 0m + }; + + public static bool IsBillableOnline(PlanTier tier) => + tier is PlanTier.Pro or PlanTier.Business; + + /// ZarinPal amounts are in Rials (۱ تومان = ۱۰ ریال). + public static long ToRials(decimal toman) => (long)(toman * 10m); +} diff --git a/src/Meezi.Core/Delivery/UnifiedDeliveryOrder.cs b/src/Meezi.Core/Delivery/UnifiedDeliveryOrder.cs new file mode 100644 index 0000000..752aca0 --- /dev/null +++ b/src/Meezi.Core/Delivery/UnifiedDeliveryOrder.cs @@ -0,0 +1,41 @@ +using Meezi.Core.Enums; + +namespace Meezi.Core.Delivery; + +public record UnifiedDeliveryCustomer( + string Name, + string Phone, + string? Address = null, + double? Lat = null, + double? Lng = null); + +public record UnifiedDeliveryItem( + string Sku, + string Name, + int Quantity, + decimal UnitPrice, + string? Notes = null); + +public record UnifiedDeliveryPayment( + decimal Total, + string Method, + bool IsPaid, + decimal? Commission = null); + +public record UnifiedDeliveryInfo( + string Type, + int? EstimatedMinutes = null, + string? DriverName = null, + string? DriverPhone = null); + +/// Normalized inbound order from Snappfood, Tap30, or other delivery webhooks. +public record UnifiedDeliveryOrder( + string ExternalId, + DeliveryPlatform Platform, + string VendorId, + DateTime CreatedAt, + UnifiedDeliveryCustomer Customer, + IReadOnlyList Items, + UnifiedDeliveryPayment Payment, + UnifiedDeliveryInfo Delivery, + UnifiedDeliveryStatus Status); diff --git a/src/Meezi.Core/Discover/CafeBadgeCatalog.cs b/src/Meezi.Core/Discover/CafeBadgeCatalog.cs new file mode 100644 index 0000000..835041b --- /dev/null +++ b/src/Meezi.Core/Discover/CafeBadgeCatalog.cs @@ -0,0 +1,36 @@ +namespace Meezi.Core.Discover; + +public record CafeBadgeDefinition(string Key, string LabelFa, string LabelEn, string LabelAr, string Icon); + +public static class CafeBadgeCatalog +{ + public static readonly IReadOnlyList All = + [ + new("verified_partner", "شریک تأییدشده", "Verified partner", "شريك موثّق", "✓"), + new("award_winner", "برنده جایزه", "Award winner", "فائز بجمة", "🏆"), + new("roastery", "رستری تخصصی", "Specialty roastery", "محمصة متخصصة", "☕"), + new("eco_friendly", "دوستدار محیط زیست", "Eco-friendly", "صديق للبيئة", "🌿"), + new("women_owned", "مدیریت زنان", "Women-owned", "إدارة نسائية", "👩"), + new("pet_friendly", "مجاز حیوان خانگی", "Pet-friendly", "يسمح بالحيوانات", "🐾"), + ]; + + private static readonly Dictionary ByKey = + All.ToDictionary(x => x.Key, StringComparer.OrdinalIgnoreCase); + + public static bool IsValidKey(string? key) => + !string.IsNullOrWhiteSpace(key) && ByKey.ContainsKey(key.Trim()); + + public static CafeBadgeDefinition? Resolve(string key) => + ByKey.TryGetValue(key.Trim(), out var def) ? def : null; + + public static IReadOnlyList NormalizeKeys(IEnumerable? keys) + { + if (keys is null) return []; + return keys + .Select(k => k.Trim()) + .Where(IsValidKey) + .Distinct(StringComparer.OrdinalIgnoreCase) + .Take(8) + .ToList(); + } +} diff --git a/src/Meezi.Core/Discover/CafeDiscoverProfile.cs b/src/Meezi.Core/Discover/CafeDiscoverProfile.cs new file mode 100644 index 0000000..4d45820 --- /dev/null +++ b/src/Meezi.Core/Discover/CafeDiscoverProfile.cs @@ -0,0 +1,76 @@ +namespace Meezi.Core.Discover; + +/// +/// Café attributes for discover / AI matching (stored as JSON on ). +/// +public class CafeDiscoverProfile +{ + /// Decor / concept themes (multi): modern, vintage, plants-heavy, … + public List Themes { get; set; } = []; + + /// Physical scale: tiny, cozy, medium, large, spacious. + public string? Size { get; set; } + + /// Floor count: one, two, three, multi. + public string? Floors { get; set; } + + /// Atmosphere tags (multi). + public List Vibes { get; set; } = []; + + /// Good for: date, family, friends, finding_someone, etc. (multi). + public List Occasions { get; set; } = []; + + /// indoor, outdoor, plants, terrace, … (multi). + public List SpaceFeatures { get; set; } = []; + + /// quiet, moderate, lively. + public string? NoiseLevel { get; set; } + + /// budget, mid, premium. + public string? PriceTier { get; set; } +} + +public static class CafeDiscoverProfileKeys +{ + public static readonly HashSet Themes = + [ + "modern", "minimal", "vintage", "industrial", "scandi", "persian_traditional", + "book_cafe", "roastery", "dessert_focus", "brunch", "late_night", + "plants_heavy", "instagrammable", "heritage", "luxury", + // extended + "specialty_coffee", "tea_house", "art_gallery", "sport_cafe", "gaming_cafe" + ]; + + public static readonly HashSet Sizes = + ["tiny", "cozy", "medium", "large", "spacious"]; + + public static readonly HashSet Floors = + ["one", "two", "three", "multi"]; + + public static readonly HashSet Vibes = + [ + "quiet", "lively", "romantic", "cozy", "trendy", "traditional", + "artistic", "luxury", "casual", "study_friendly" + ]; + + public static readonly HashSet Occasions = + [ + "date", "family", "friends", "finding_someone", "solo", + "business_meeting", "study_work", "celebration", + "quick_coffee", "breakfast", "brunch", + // extended + "after_dinner", "group_large" + ]; + + public static readonly HashSet SpaceFeatures = + [ + "indoor", "outdoor", "terrace", "rooftop", "garden", "plants", + "wifi", "parking", "wheelchair", "kids_friendly", "pet_friendly", + "smoking_area", "live_music", "private_room", "counter_only", + // extended + "takeaway", "hookah", "board_games", "no_smoking", "prayer_room" + ]; + + public static readonly HashSet NoiseLevels = ["quiet", "moderate", "lively"]; + public static readonly HashSet PriceTiers = ["budget", "mid", "premium"]; +} diff --git a/src/Meezi.Core/Discover/WorkingHours.cs b/src/Meezi.Core/Discover/WorkingHours.cs new file mode 100644 index 0000000..cc93219 --- /dev/null +++ b/src/Meezi.Core/Discover/WorkingHours.cs @@ -0,0 +1,80 @@ +namespace Meezi.Core.Discover; + +/// Opening hours for a single day. +public class DaySchedule +{ + public bool IsOpen { get; set; } + /// 24-h HH:mm, e.g. "08:00" + public string? Open { get; set; } + /// 24-h HH:mm, e.g. "23:30". If past midnight, still next-calendar-day open time. + public string? Close { get; set; } +} + +/// +/// Full-week schedule. Iran week: Saturday = first day, Friday = last day. +/// Stored as JSON on . +/// +public class WorkingHoursSchedule +{ + public DaySchedule? Sat { get; set; } + public DaySchedule? Sun { get; set; } + public DaySchedule? Mon { get; set; } + public DaySchedule? Tue { get; set; } + public DaySchedule? Wed { get; set; } + public DaySchedule? Thu { get; set; } + public DaySchedule? Fri { get; set; } + + /// + /// Returns the for the given UTC day-of-week, + /// adjusted to Iran Standard Time (UTC+3:30). + /// + public DaySchedule? ForUtcNow() + { + // Iran Standard Time: UTC+03:30 (no DST) + var iranOffset = TimeSpan.FromMinutes(210); + var iranNow = DateTimeOffset.UtcNow.ToOffset(iranOffset); + return iranNow.DayOfWeek switch + { + DayOfWeek.Saturday => Sat, + DayOfWeek.Sunday => Sun, + DayOfWeek.Monday => Mon, + DayOfWeek.Tuesday => Tue, + DayOfWeek.Wednesday => Wed, + DayOfWeek.Thursday => Thu, + DayOfWeek.Friday => Fri, + _ => null + }; + } + + /// Returns true when the cafe is currently open based on Iran time. + public bool IsOpenNow() + { + var day = ForUtcNow(); + if (day is null || !day.IsOpen) return false; + if (string.IsNullOrEmpty(day.Open) || string.IsNullOrEmpty(day.Close)) return true; // open all day + + var iranOffset = TimeSpan.FromMinutes(210); + var iranNow = DateTimeOffset.UtcNow.ToOffset(iranOffset); + var nowMinutes = iranNow.Hour * 60 + iranNow.Minute; + + if (!TryParseMinutes(day.Open, out var openMin) || !TryParseMinutes(day.Close, out var closeMin)) + return true; + + // Handle spans crossing midnight (e.g. 22:00–02:00) + if (closeMin <= openMin) + return nowMinutes >= openMin || nowMinutes < closeMin; + + return nowMinutes >= openMin && nowMinutes < closeMin; + } + + private static bool TryParseMinutes(string? hhmm, out int minutes) + { + minutes = 0; + if (string.IsNullOrEmpty(hhmm)) return false; + var parts = hhmm.Split(':'); + if (parts.Length != 2) return false; + if (!int.TryParse(parts[0], out var h) || !int.TryParse(parts[1], out var m)) return false; + minutes = h * 60 + m; + return true; + } +} diff --git a/src/Meezi.Core/Entities/Attendance.cs b/src/Meezi.Core/Entities/Attendance.cs new file mode 100644 index 0000000..efe161f --- /dev/null +++ b/src/Meezi.Core/Entities/Attendance.cs @@ -0,0 +1,12 @@ +namespace Meezi.Core.Entities; + +public class Attendance : BaseEntity +{ + public string EmployeeId { get; set; } = string.Empty; + public DateOnly Date { get; set; } + public DateTime? ClockIn { get; set; } + public DateTime? ClockOut { get; set; } + public string? Notes { get; set; } + + public Employee Employee { get; set; } = null!; +} diff --git a/src/Meezi.Core/Entities/BaseEntity.cs b/src/Meezi.Core/Entities/BaseEntity.cs new file mode 100644 index 0000000..8df9421 --- /dev/null +++ b/src/Meezi.Core/Entities/BaseEntity.cs @@ -0,0 +1,13 @@ +namespace Meezi.Core.Entities; + +public abstract class BaseEntity +{ + public string Id { get; set; } = Guid.NewGuid().ToString("N"); + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public DateTime? DeletedAt { get; set; } +} + +public abstract class TenantEntity : BaseEntity +{ + public string CafeId { get; set; } = string.Empty; +} diff --git a/src/Meezi.Core/Entities/Branch.cs b/src/Meezi.Core/Entities/Branch.cs new file mode 100644 index 0000000..151aefc --- /dev/null +++ b/src/Meezi.Core/Entities/Branch.cs @@ -0,0 +1,42 @@ +namespace Meezi.Core.Entities; + +/// Physical branch (شعبه) under a café tenant. +public class Branch : TenantEntity +{ + public string Name { get; set; } = string.Empty; + public string? Address { get; set; } + public string? City { get; set; } + public string? Phone { get; set; } + public bool IsActive { get; set; } = true; + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; + + /// When set with , branch can be restored until this UTC time. + public DateTime? ScheduledPermanentDeleteAt { get; set; } + + // Thermal printer (TCP ESC/POS) + public string? ReceiptPrinterIp { get; set; } + public int? ReceiptPrinterPort { get; set; } + public string? KitchenPrinterIp { get; set; } + public int? KitchenPrinterPort { get; set; } + public int PaperWidthMm { get; set; } = 80; + public bool AutoCutEnabled { get; set; } = true; + public string? ReceiptHeader { get; set; } + public string? ReceiptFooter { get; set; } + public string? WifiPassword { get; set; } + /// Branch-specific logo on QR guest menu (falls back to café logo). + public string? LogoUrl { get; set; } + public string? WelcomeText { get; set; } + public string? AccentColor { get; set; } + /// Branch tax % when café is true. + public decimal? TaxRate { get; set; } + + // Card POS terminal (HTTP bridge on local network) + public string? PosDeviceIp { get; set; } + public int? PosDevicePort { get; set; } + + public Cafe Cafe { get; set; } = null!; + public ICollection Sections { get; set; } = []; + public ICollection
Tables { get; set; } = []; + public ICollection Orders { get; set; } = []; + public ICollection Staff { get; set; } = []; +} diff --git a/src/Meezi.Core/Entities/BranchMenuItemOverride.cs b/src/Meezi.Core/Entities/BranchMenuItemOverride.cs new file mode 100644 index 0000000..5ac3901 --- /dev/null +++ b/src/Meezi.Core/Entities/BranchMenuItemOverride.cs @@ -0,0 +1,18 @@ +namespace Meezi.Core.Entities; + +/// Per-branch availability and price override for a catalog menu item. +public class BranchMenuItemOverride : TenantEntity +{ + public string BranchId { get; set; } = string.Empty; + public string MenuItemId { get; set; } = string.Empty; + /// false = hidden at this branch. + public bool IsAvailable { get; set; } = true; + /// null = use MenuItem.Price. + public decimal? PriceOverride { get; set; } + public int? SortOrderOverride { get; set; } + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; + public string? UpdatedByUserId { get; set; } + + public Branch Branch { get; set; } = null!; + public MenuItem MenuItem { get; set; } = null!; +} diff --git a/src/Meezi.Core/Entities/Cafe.cs b/src/Meezi.Core/Entities/Cafe.cs new file mode 100644 index 0000000..129b6a8 --- /dev/null +++ b/src/Meezi.Core/Entities/Cafe.cs @@ -0,0 +1,56 @@ +using Meezi.Core.Enums; + +namespace Meezi.Core.Entities; + +public class Cafe : BaseEntity +{ + public string Name { get; set; } = string.Empty; + public string? NameAr { get; set; } + public string? NameEn { get; set; } + public string Slug { get; set; } = string.Empty; + public string? Phone { get; set; } + public string? Address { get; set; } + public string? City { get; set; } + public string? LogoUrl { get; set; } + public string? CoverImageUrl { get; set; } + public string? Description { get; set; } + public PlanTier PlanTier { get; set; } = PlanTier.Free; + public DateTime? PlanExpiresAt { get; set; } + public bool IsVerified { get; set; } + /// When true, merchant API access is blocked until reactivated by platform admin. + public bool IsSuspended { get; set; } + public string? SnappfoodVendorId { get; set; } + public string? Tap30VendorId { get; set; } + public string? DigikalaVendorId { get; set; } + public string PreferredLanguage { get; set; } = "fa"; + /// JSON for dashboard + guest menu colors/styles. + public string? ThemeJson { get; set; } + /// JSON for discover / AI matching. + public string? DiscoverProfileJson { get; set; } + /// JSON array of keys (Enterprise, admin-assigned). + public string? DiscoverBadgesJson { get; set; } + /// JSON array of up to 8 gallery photo URLs (owner-managed). + public string? GalleryJson { get; set; } + /// JSON per-day schedule. + public string? WorkingHoursJson { get; set; } + /// Instagram handle without @, max 80 chars. + public string? InstagramHandle { get; set; } + /// Cafe website URL, max 300 chars. + public string? WebsiteUrl { get; set; } + /// Default VAT/sales tax % for all branches unless branch override is allowed. + public decimal DefaultTaxRate { get; set; } = 9m; + public bool AllowBranchTaxOverride { get; set; } + + public ICollection Branches { get; set; } = []; + public ICollection
Tables { get; set; } = []; + public ICollection Employees { get; set; } = []; + public ICollection MenuCategories { get; set; } = []; + public ICollection MenuItems { get; set; } = []; + public ICollection Orders { get; set; } = []; + public ICollection Customers { get; set; } = []; + public ICollection Coupons { get; set; } = []; + public ICollection Taxes { get; set; } = []; + public ICollection Reviews { get; set; } = []; + public ICollection SubscriptionPayments { get; set; } = []; + public ICollection Ingredients { get; set; } = []; +} diff --git a/src/Meezi.Core/Entities/CafeFeatureOverride.cs b/src/Meezi.Core/Entities/CafeFeatureOverride.cs new file mode 100644 index 0000000..28236d2 --- /dev/null +++ b/src/Meezi.Core/Entities/CafeFeatureOverride.cs @@ -0,0 +1,7 @@ +namespace Meezi.Core.Entities; + +public class CafeFeatureOverride : TenantEntity +{ + public string FeatureKey { get; set; } = string.Empty; + public bool IsEnabled { get; set; } +} diff --git a/src/Meezi.Core/Entities/CafeNotification.cs b/src/Meezi.Core/Entities/CafeNotification.cs new file mode 100644 index 0000000..e674860 --- /dev/null +++ b/src/Meezi.Core/Entities/CafeNotification.cs @@ -0,0 +1,12 @@ +namespace Meezi.Core.Entities; + +public class CafeNotification : TenantEntity +{ + public string Type { get; set; } = string.Empty; + public string Title { get; set; } = string.Empty; + public string? Body { get; set; } + public string? ReferenceId { get; set; } + public string? TableNumber { get; set; } + public bool IsRead { get; set; } + public DateTime? ReadAt { get; set; } +} diff --git a/src/Meezi.Core/Entities/CafeReview.cs b/src/Meezi.Core/Entities/CafeReview.cs new file mode 100644 index 0000000..4b83438 --- /dev/null +++ b/src/Meezi.Core/Entities/CafeReview.cs @@ -0,0 +1,16 @@ +namespace Meezi.Core.Entities; + +public class CafeReview : BaseEntity +{ + public string CafeId { get; set; } = string.Empty; + public string AuthorName { get; set; } = string.Empty; + public string? AuthorPhone { get; set; } + public int Rating { get; set; } + public string? Comment { get; set; } + public string? OwnerReply { get; set; } + public DateTime? OwnerRepliedAt { get; set; } + public bool IsHidden { get; set; } + + public Cafe Cafe { get; set; } = null!; + public ICollection Photos { get; set; } = []; +} diff --git a/src/Meezi.Core/Entities/CafeReviewPhoto.cs b/src/Meezi.Core/Entities/CafeReviewPhoto.cs new file mode 100644 index 0000000..72c78c9 --- /dev/null +++ b/src/Meezi.Core/Entities/CafeReviewPhoto.cs @@ -0,0 +1,10 @@ +namespace Meezi.Core.Entities; + +public class CafeReviewPhoto : BaseEntity +{ + public string ReviewId { get; set; } = string.Empty; + public string Url { get; set; } = string.Empty; + public int SortOrder { get; set; } + + public CafeReview Review { get; set; } = null!; +} diff --git a/src/Meezi.Core/Entities/CashTransaction.cs b/src/Meezi.Core/Entities/CashTransaction.cs new file mode 100644 index 0000000..0577420 --- /dev/null +++ b/src/Meezi.Core/Entities/CashTransaction.cs @@ -0,0 +1,18 @@ +using Meezi.Core.Enums; + +namespace Meezi.Core.Entities; + +public class CashTransaction : TenantEntity +{ + public string ShiftId { get; set; } = string.Empty; + public string? BranchId { get; set; } + public CashTransactionType Type { get; set; } + public PaymentMethod Method { get; set; } + public decimal Amount { get; set; } + public string? ReferenceId { get; set; } + public string? Note { get; set; } + public string CreatedByUserId { get; set; } = string.Empty; + + public Shift Shift { get; set; } = null!; + public Branch? Branch { get; set; } +} diff --git a/src/Meezi.Core/Entities/ConsumerAccount.cs b/src/Meezi.Core/Entities/ConsumerAccount.cs new file mode 100644 index 0000000..0be1c9c --- /dev/null +++ b/src/Meezi.Core/Entities/ConsumerAccount.cs @@ -0,0 +1,8 @@ +namespace Meezi.Core.Entities; + +/// Platform guest account (OTP login) for order history across cafés. +public class ConsumerAccount : BaseEntity +{ + public string Phone { get; set; } = string.Empty; + public string? Name { get; set; } +} diff --git a/src/Meezi.Core/Entities/Coupon.cs b/src/Meezi.Core/Entities/Coupon.cs new file mode 100644 index 0000000..09423e0 --- /dev/null +++ b/src/Meezi.Core/Entities/Coupon.cs @@ -0,0 +1,21 @@ +using Meezi.Core.Enums; + +namespace Meezi.Core.Entities; + +public class Coupon : TenantEntity +{ + public string Code { get; set; } = string.Empty; + public CouponType Type { get; set; } + public decimal Value { get; set; } + public decimal? MinOrderAmount { get; set; } + public decimal? MaxDiscount { get; set; } + public int? UsageLimit { get; set; } + public int UsedCount { get; set; } + public CustomerGroup? TargetGroup { get; set; } + public DateTime? StartsAt { get; set; } + public DateTime? ExpiresAt { get; set; } + public bool IsActive { get; set; } = true; + + public Cafe Cafe { get; set; } = null!; + public ICollection Orders { get; set; } = []; +} diff --git a/src/Meezi.Core/Entities/Customer.cs b/src/Meezi.Core/Entities/Customer.cs new file mode 100644 index 0000000..6dc3bd0 --- /dev/null +++ b/src/Meezi.Core/Entities/Customer.cs @@ -0,0 +1,17 @@ +using Meezi.Core.Enums; + +namespace Meezi.Core.Entities; + +public class Customer : TenantEntity +{ + public string Name { get; set; } = string.Empty; + public string Phone { get; set; } = string.Empty; + public string? NationalId { get; set; } + public string? BirthDateJalali { get; set; } + public CustomerGroup Group { get; set; } = CustomerGroup.Regular; + public int LoyaltyPoints { get; set; } + public string? ReferredBy { get; set; } + + public Cafe Cafe { get; set; } = null!; + public ICollection Orders { get; set; } = []; +} diff --git a/src/Meezi.Core/Entities/DailyReport.cs b/src/Meezi.Core/Entities/DailyReport.cs new file mode 100644 index 0000000..a3cff66 --- /dev/null +++ b/src/Meezi.Core/Entities/DailyReport.cs @@ -0,0 +1,22 @@ +namespace Meezi.Core.Entities; + +/// Pre-aggregated daily metrics per branch for fast dashboard reads. +public class DailyReport : TenantEntity +{ + public string BranchId { get; set; } = string.Empty; + public DateOnly Date { get; set; } + public decimal TotalRevenue { get; set; } + public decimal CashRevenue { get; set; } + public decimal CardRevenue { get; set; } + public decimal CreditRevenue { get; set; } + public int TotalOrders { get; set; } + public decimal AvgOrderValue { get; set; } + public int TotalVoids { get; set; } + public decimal VoidAmount { get; set; } + public decimal TotalExpenses { get; set; } + public decimal NetIncome { get; set; } + public List TopProducts { get; set; } = []; + public DateTime GeneratedAt { get; set; } = DateTime.UtcNow; + + public Branch Branch { get; set; } = null!; +} diff --git a/src/Meezi.Core/Entities/DeliveryCommissionRate.cs b/src/Meezi.Core/Entities/DeliveryCommissionRate.cs new file mode 100644 index 0000000..a3a5b22 --- /dev/null +++ b/src/Meezi.Core/Entities/DeliveryCommissionRate.cs @@ -0,0 +1,14 @@ +using Meezi.Core.Enums; + +namespace Meezi.Core.Entities; + +/// Configurable platform commission % per café (falls back to app defaults). +public class DeliveryCommissionRate : TenantEntity +{ + public DeliveryPlatform Platform { get; set; } + public decimal RatePercent { get; set; } + public bool IsActive { get; set; } = true; + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; + + public Cafe Cafe { get; set; } = null!; +} diff --git a/src/Meezi.Core/Entities/DemoRequest.cs b/src/Meezi.Core/Entities/DemoRequest.cs new file mode 100644 index 0000000..b09ee5e --- /dev/null +++ b/src/Meezi.Core/Entities/DemoRequest.cs @@ -0,0 +1,17 @@ +using Meezi.Core.Enums; + +namespace Meezi.Core.Entities; + +public class DemoRequest : BaseEntity +{ + public string ContactName { get; set; } = string.Empty; + public string BusinessName { get; set; } = string.Empty; + public string Phone { get; set; } = string.Empty; + public string? Email { get; set; } + public string BranchCount { get; set; } = "1"; + public string? Notes { get; set; } + public string Source { get; set; } = "website"; + public DemoRequestStatus Status { get; set; } = DemoRequestStatus.New; + public string? AdminNotes { get; set; } + public DateTime? ContactedAt { get; set; } +} diff --git a/src/Meezi.Core/Entities/Employee.cs b/src/Meezi.Core/Entities/Employee.cs new file mode 100644 index 0000000..8add810 --- /dev/null +++ b/src/Meezi.Core/Entities/Employee.cs @@ -0,0 +1,22 @@ +using Meezi.Core.Enums; + +namespace Meezi.Core.Entities; + +public class Employee : TenantEntity +{ + public string? BranchId { get; set; } + public string Name { get; set; } = string.Empty; + public string Phone { get; set; } = string.Empty; + public string? NationalId { get; set; } + public EmployeeRole Role { get; set; } + public decimal BaseSalary { get; set; } + public string? PinCode { get; set; } + + public Cafe Cafe { get; set; } = null!; + public Branch? Branch { get; set; } + public ICollection Orders { get; set; } = []; + public ICollection Salaries { get; set; } = []; + public ICollection Attendances { get; set; } = []; + public ICollection Schedules { get; set; } = []; + public ICollection LeaveRequests { get; set; } = []; +} diff --git a/src/Meezi.Core/Entities/EmployeeSalary.cs b/src/Meezi.Core/Entities/EmployeeSalary.cs new file mode 100644 index 0000000..49d359b --- /dev/null +++ b/src/Meezi.Core/Entities/EmployeeSalary.cs @@ -0,0 +1,14 @@ +namespace Meezi.Core.Entities; + +public class EmployeeSalary : BaseEntity +{ + public string EmployeeId { get; set; } = string.Empty; + public string MonthYear { get; set; } = string.Empty; + public decimal BaseSalary { get; set; } + public decimal OvertimePay { get; set; } + public decimal Deductions { get; set; } + public decimal NetSalary { get; set; } + public bool IsPaid { get; set; } + + public Employee Employee { get; set; } = null!; +} diff --git a/src/Meezi.Core/Entities/EmployeeSchedule.cs b/src/Meezi.Core/Entities/EmployeeSchedule.cs new file mode 100644 index 0000000..a28cb37 --- /dev/null +++ b/src/Meezi.Core/Entities/EmployeeSchedule.cs @@ -0,0 +1,13 @@ +using Meezi.Core.Enums; + +namespace Meezi.Core.Entities; + +/// Weekly HR work schedule for an employee (not cash register). +public class EmployeeSchedule : BaseEntity +{ + public string EmployeeId { get; set; } = string.Empty; + public int DayOfWeek { get; set; } + public ShiftType ShiftType { get; set; } + + public Employee Employee { get; set; } = null!; +} diff --git a/src/Meezi.Core/Entities/Expense.cs b/src/Meezi.Core/Entities/Expense.cs new file mode 100644 index 0000000..22cbc41 --- /dev/null +++ b/src/Meezi.Core/Entities/Expense.cs @@ -0,0 +1,17 @@ +using Meezi.Core.Enums; + +namespace Meezi.Core.Entities; + +public class Expense : TenantEntity +{ + public string BranchId { get; set; } = string.Empty; + public string? ShiftId { get; set; } + public ExpenseCategory Category { get; set; } + public decimal Amount { get; set; } + public string? Note { get; set; } + public string? ReceiptImageUrl { get; set; } + public string CreatedByUserId { get; set; } = string.Empty; + + public Branch Branch { get; set; } = null!; + public Shift? Shift { get; set; } +} diff --git a/src/Meezi.Core/Entities/Ingredient.cs b/src/Meezi.Core/Entities/Ingredient.cs new file mode 100644 index 0000000..c62727d --- /dev/null +++ b/src/Meezi.Core/Entities/Ingredient.cs @@ -0,0 +1,19 @@ +namespace Meezi.Core.Entities; + +public class Ingredient : TenantEntity +{ + public string Name { get; set; } = string.Empty; + public string Unit { get; set; } = "عدد"; + public decimal QuantityOnHand { get; set; } + public decimal ReorderLevel { get; set; } + /// Cost per unit (Toman) for COGS reporting. + public decimal UnitCost { get; set; } + /// Target / full stock level used with warning percent (e.g. 500 g). + public decimal ParLevel { get; set; } + /// Warn when on-hand falls below ParLevel × (this ÷ 100). Default 20 = 20%. + public decimal LowStockWarningPercent { get; set; } = 20m; + + public Cafe Cafe { get; set; } = null!; + public ICollection Movements { get; set; } = []; + public ICollection MenuItemRecipes { get; set; } = []; +} diff --git a/src/Meezi.Core/Entities/KitchenStation.cs b/src/Meezi.Core/Entities/KitchenStation.cs new file mode 100644 index 0000000..1b78221 --- /dev/null +++ b/src/Meezi.Core/Entities/KitchenStation.cs @@ -0,0 +1,15 @@ +namespace Meezi.Core.Entities; + +/// Kitchen/bar print station — routes category items to a dedicated printer. +public class KitchenStation : TenantEntity +{ + public string? BranchId { get; set; } + public string Name { get; set; } = string.Empty; + public string? PrinterIp { get; set; } + public int PrinterPort { get; set; } = 9100; + public int SortOrder { get; set; } + + public Cafe Cafe { get; set; } = null!; + public Branch? Branch { get; set; } + public ICollection Categories { get; set; } = []; +} diff --git a/src/Meezi.Core/Entities/LeaveRequest.cs b/src/Meezi.Core/Entities/LeaveRequest.cs new file mode 100644 index 0000000..2e462ea --- /dev/null +++ b/src/Meezi.Core/Entities/LeaveRequest.cs @@ -0,0 +1,15 @@ +using Meezi.Core.Enums; + +namespace Meezi.Core.Entities; + +public class LeaveRequest : BaseEntity +{ + public string EmployeeId { get; set; } = string.Empty; + public DateOnly StartDate { get; set; } + public DateOnly EndDate { get; set; } + public string? Reason { get; set; } + public LeaveStatus Status { get; set; } = LeaveStatus.Pending; + public string? ReviewedBy { get; set; } + + public Employee Employee { get; set; } = null!; +} diff --git a/src/Meezi.Core/Entities/MenuCategory.cs b/src/Meezi.Core/Entities/MenuCategory.cs new file mode 100644 index 0000000..7e31e58 --- /dev/null +++ b/src/Meezi.Core/Entities/MenuCategory.cs @@ -0,0 +1,25 @@ +namespace Meezi.Core.Entities; + +public class MenuCategory : TenantEntity +{ + public string Name { get; set; } = string.Empty; + public string? NameAr { get; set; } + public string? NameEn { get; set; } + public int SortOrder { get; set; } + public string? TaxId { get; set; } + public decimal DiscountPercent { get; set; } + /// Emoji or short icon label shown when no preset/image is set. + public string? Icon { get; set; } + /// Preset icon id (see CategoryIconPresets.PresetId). + public string? IconPresetId { get; set; } + /// Preset visual style: flat, modern, real, minimal, outline. + public string? IconStyle { get; set; } + public string? ImageUrl { get; set; } + public bool IsActive { get; set; } = true; + public string? KitchenStationId { get; set; } + + public Cafe Cafe { get; set; } = null!; + public KitchenStation? KitchenStation { get; set; } + public Tax? Tax { get; set; } + public ICollection MenuItems { get; set; } = []; +} diff --git a/src/Meezi.Core/Entities/MenuItem.cs b/src/Meezi.Core/Entities/MenuItem.cs new file mode 100644 index 0000000..973312c --- /dev/null +++ b/src/Meezi.Core/Entities/MenuItem.cs @@ -0,0 +1,24 @@ +namespace Meezi.Core.Entities; + +public class MenuItem : TenantEntity +{ + public string CategoryId { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public string? NameAr { get; set; } + public string? NameEn { get; set; } + public string? Description { get; set; } + public decimal Price { get; set; } + /// Item-level promo (0–100). Takes precedence over category discount when > 0. + public decimal DiscountPercent { get; set; } + public string? ImageUrl { get; set; } + public string? VideoUrl { get; set; } + /// GLB/GLTF 3D model for QR menu interactive view (poster = ImageUrl). + public string? Model3dUrl { get; set; } + public bool IsAvailable { get; set; } = true; + + public Cafe Cafe { get; set; } = null!; + public MenuCategory Category { get; set; } = null!; + public ICollection OrderItems { get; set; } = []; + public ICollection BranchOverrides { get; set; } = []; + public ICollection RecipeIngredients { get; set; } = []; +} diff --git a/src/Meezi.Core/Entities/MenuItemIngredient.cs b/src/Meezi.Core/Entities/MenuItemIngredient.cs new file mode 100644 index 0000000..78015c6 --- /dev/null +++ b/src/Meezi.Core/Entities/MenuItemIngredient.cs @@ -0,0 +1,13 @@ +namespace Meezi.Core.Entities; + +/// Bill of materials: how much of an ingredient each menu item consumes per unit sold. +public class MenuItemIngredient : TenantEntity +{ + public string MenuItemId { get; set; } = string.Empty; + public string IngredientId { get; set; } = string.Empty; + /// Amount of ingredient used per 1 sold unit (e.g. 10 g coffee per espresso). + public decimal QuantityPerUnit { get; set; } + + public MenuItem MenuItem { get; set; } = null!; + public Ingredient Ingredient { get; set; } = null!; +} diff --git a/src/Meezi.Core/Entities/Order.cs b/src/Meezi.Core/Entities/Order.cs new file mode 100644 index 0000000..7b50dc3 --- /dev/null +++ b/src/Meezi.Core/Entities/Order.cs @@ -0,0 +1,46 @@ +using Meezi.Core.Enums; + +namespace Meezi.Core.Entities; + +public class Order : TenantEntity +{ + public string? BranchId { get; set; } + public string? TableId { get; set; } + public string? ReservationId { get; set; } + /// Walk-in label at POS when no CRM customer is linked. + public string? GuestName { get; set; } + public string? GuestPhone { get; set; } + public string? CustomerId { get; set; } + public string? EmployeeId { get; set; } + public OrderType OrderType { get; set; } + public OrderSource Source { get; set; } = OrderSource.Pos; + public OrderStatus Status { get; set; } = OrderStatus.Pending; + /// Human-facing order number (digits only), unique per cafe. + public int DisplayNumber { get; set; } + public DateTime StatusUpdatedAt { get; set; } = DateTime.UtcNow; + /// Secret token for guest order tracking (QR) without login. + public string? GuestTrackingToken { get; set; } + public string? CouponId { get; set; } + public decimal DiscountAmount { get; set; } + public decimal Subtotal { get; set; } + public decimal TaxTotal { get; set; } + public decimal Total { get; set; } + public string? SnappfoodOrderId { get; set; } + /// External order id from delivery platform (Snappfood, Tap30, etc.). + public string? ExternalOrderId { get; set; } + public DeliveryPlatform? DeliveryPlatform { get; set; } + /// Platform commission amount deducted from gross (stored for finance reports). + public decimal PlatformCommission { get; set; } + /// JSON snapshot: driver, address, delivery ETA, etc. + public string? DeliveryMetaJson { get; set; } + + public Cafe Cafe { get; set; } = null!; + public Branch? Branch { get; set; } + public Table? Table { get; set; } + public TableReservation? Reservation { get; set; } + public Customer? Customer { get; set; } + public Employee? Employee { get; set; } + public Coupon? Coupon { get; set; } + public ICollection Items { get; set; } = []; + public ICollection Payments { get; set; } = []; +} diff --git a/src/Meezi.Core/Entities/OrderItem.cs b/src/Meezi.Core/Entities/OrderItem.cs new file mode 100644 index 0000000..57506ef --- /dev/null +++ b/src/Meezi.Core/Entities/OrderItem.cs @@ -0,0 +1,16 @@ +namespace Meezi.Core.Entities; + +public class OrderItem : BaseEntity +{ + public string OrderId { get; set; } = string.Empty; + public string MenuItemId { get; set; } = string.Empty; + public int Quantity { get; set; } + public decimal UnitPrice { get; set; } + public string? Notes { get; set; } + public bool IsVoided { get; set; } + public DateTime? VoidedAt { get; set; } + public string? VoidedByUserId { get; set; } + + public Order Order { get; set; } = null!; + public MenuItem MenuItem { get; set; } = null!; +} diff --git a/src/Meezi.Core/Entities/Payment.cs b/src/Meezi.Core/Entities/Payment.cs new file mode 100644 index 0000000..5ff6521 --- /dev/null +++ b/src/Meezi.Core/Entities/Payment.cs @@ -0,0 +1,14 @@ +using Meezi.Core.Enums; + +namespace Meezi.Core.Entities; + +public class Payment : BaseEntity +{ + public string OrderId { get; set; } = string.Empty; + public PaymentMethod Method { get; set; } + public decimal Amount { get; set; } + public PaymentStatus Status { get; set; } = PaymentStatus.Pending; + public string? Reference { get; set; } + + public Order Order { get; set; } = null!; +} diff --git a/src/Meezi.Core/Entities/PlatformFeature.cs b/src/Meezi.Core/Entities/PlatformFeature.cs new file mode 100644 index 0000000..00abe26 --- /dev/null +++ b/src/Meezi.Core/Entities/PlatformFeature.cs @@ -0,0 +1,11 @@ +namespace Meezi.Core.Entities; + +public class PlatformFeature : BaseEntity +{ + public string Key { get; set; } = string.Empty; + public string DisplayNameFa { get; set; } = string.Empty; + public string? DisplayNameEn { get; set; } + public string ModuleGroup { get; set; } = "general"; + public bool IsEnabledGlobally { get; set; } = true; + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; +} diff --git a/src/Meezi.Core/Entities/PlatformPlanDefinition.cs b/src/Meezi.Core/Entities/PlatformPlanDefinition.cs new file mode 100644 index 0000000..9aa184f --- /dev/null +++ b/src/Meezi.Core/Entities/PlatformPlanDefinition.cs @@ -0,0 +1,18 @@ +using Meezi.Core.Enums; + +namespace Meezi.Core.Entities; + +public class PlatformPlanDefinition : BaseEntity +{ + public PlanTier Tier { get; set; } + public string DisplayNameFa { get; set; } = string.Empty; + public string? DisplayNameEn { get; set; } + public decimal MonthlyPriceToman { get; set; } + public bool IsBillableOnline { get; set; } + public bool IsActive { get; set; } = true; + public int SortOrder { get; set; } + /// JSON . + public string LimitsJson { get; set; } = "{}"; + /// JSON array of feature keys enabled on this plan. + public string? FeaturesJson { get; set; } +} diff --git a/src/Meezi.Core/Entities/PlatformSetting.cs b/src/Meezi.Core/Entities/PlatformSetting.cs new file mode 100644 index 0000000..d3f7a85 --- /dev/null +++ b/src/Meezi.Core/Entities/PlatformSetting.cs @@ -0,0 +1,10 @@ +namespace Meezi.Core.Entities; + +public class PlatformSetting : BaseEntity +{ + public string Key { get; set; } = string.Empty; + public string Value { get; set; } = string.Empty; + public string Category { get; set; } = "general"; + public string? DescriptionFa { get; set; } + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; +} diff --git a/src/Meezi.Core/Entities/QueueTicket.cs b/src/Meezi.Core/Entities/QueueTicket.cs new file mode 100644 index 0000000..e4c3b5c --- /dev/null +++ b/src/Meezi.Core/Entities/QueueTicket.cs @@ -0,0 +1,20 @@ +using Meezi.Core.Enums; + +namespace Meezi.Core.Entities; + +/// Daily take-a-number ticket; sequence resets each calendar day (branch-local). +public class QueueTicket : TenantEntity +{ + public string? BranchId { get; set; } + public DateOnly ServiceDate { get; set; } + public int Number { get; set; } + public string? CustomerLabel { get; set; } + public string? IssuedByUserId { get; set; } + public QueueTicketStatus Status { get; set; } = QueueTicketStatus.Waiting; + public string? OrderId { get; set; } + public DateTime IssuedAt { get; set; } + + public Cafe Cafe { get; set; } = null!; + public Branch? Branch { get; set; } + public Order? Order { get; set; } +} diff --git a/src/Meezi.Core/Entities/Shift.cs b/src/Meezi.Core/Entities/Shift.cs new file mode 100644 index 0000000..6bcce2e --- /dev/null +++ b/src/Meezi.Core/Entities/Shift.cs @@ -0,0 +1,24 @@ +using Meezi.Core.Enums; + +namespace Meezi.Core.Entities; + +/// Cash register session (باز / بسته کردن صندوق) for a branch. +public class Shift : TenantEntity +{ + public string BranchId { get; set; } = string.Empty; + public string OpenedByUserId { get; set; } = string.Empty; + public string? ClosedByUserId { get; set; } + public DateTime OpenedAt { get; set; } + public DateTime? ClosedAt { get; set; } + public decimal OpeningCash { get; set; } + public decimal? ClosingCash { get; set; } + public decimal ExpectedCash { get; set; } + public decimal? Discrepancy { get; set; } + public ShiftStatus Status { get; set; } = ShiftStatus.Open; + + public Cafe Cafe { get; set; } = null!; + public Branch Branch { get; set; } = null!; + public Employee OpenedBy { get; set; } = null!; + public Employee? ClosedBy { get; set; } + public ICollection Transactions { get; set; } = []; +} diff --git a/src/Meezi.Core/Entities/StockMovement.cs b/src/Meezi.Core/Entities/StockMovement.cs new file mode 100644 index 0000000..7b61fb1 --- /dev/null +++ b/src/Meezi.Core/Entities/StockMovement.cs @@ -0,0 +1,20 @@ +using Meezi.Core.Enums; + +namespace Meezi.Core.Entities; + +public class StockMovement : TenantEntity +{ + public string IngredientId { get; set; } = string.Empty; + public string? BranchId { get; set; } + public decimal Delta { get; set; } + public StockMovementKind Kind { get; set; } = StockMovementKind.Manual; + public string? OrderId { get; set; } + /// Total amount paid for this movement (Toman), when stock is received. + public decimal? TotalCostToman { get; set; } + /// Linked expense row for budget / reports (purchase only). + public string? ExpenseId { get; set; } + public string? Note { get; set; } + + public Cafe Cafe { get; set; } = null!; + public Ingredient Ingredient { get; set; } = null!; +} diff --git a/src/Meezi.Core/Entities/SubscriptionPayment.cs b/src/Meezi.Core/Entities/SubscriptionPayment.cs new file mode 100644 index 0000000..6f899bf --- /dev/null +++ b/src/Meezi.Core/Entities/SubscriptionPayment.cs @@ -0,0 +1,17 @@ +using Meezi.Core.Enums; + +namespace Meezi.Core.Entities; + +public class SubscriptionPayment : TenantEntity +{ + public PlanTier PlanTier { get; set; } + public int Months { get; set; } + public decimal AmountToman { get; set; } + public long AmountRials { get; set; } + public PaymentProvider Provider { get; set; } = PaymentProvider.ZarinPal; + public string? Authority { get; set; } + public string? RefId { get; set; } + public SubscriptionPaymentStatus Status { get; set; } = SubscriptionPaymentStatus.Pending; + + public Cafe Cafe { get; set; } = null!; +} diff --git a/src/Meezi.Core/Entities/SupportTicket.cs b/src/Meezi.Core/Entities/SupportTicket.cs new file mode 100644 index 0000000..7853eb8 --- /dev/null +++ b/src/Meezi.Core/Entities/SupportTicket.cs @@ -0,0 +1,18 @@ +using Meezi.Core.Enums; + +namespace Meezi.Core.Entities; + +public class SupportTicket : TenantEntity +{ + public string Subject { get; set; } = string.Empty; + public SupportTicketStatus Status { get; set; } = SupportTicketStatus.Open; + public SupportTicketPriority Priority { get; set; } = SupportTicketPriority.Normal; + public string CreatedByEmployeeId { get; set; } = string.Empty; + public string? AssignedAdminId { get; set; } + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; + public DateTime? ClosedAt { get; set; } + + public Cafe? Cafe { get; set; } + public Employee? CreatedByEmployee { get; set; } + public ICollection Messages { get; set; } = []; +} diff --git a/src/Meezi.Core/Entities/SupportTicketMessage.cs b/src/Meezi.Core/Entities/SupportTicketMessage.cs new file mode 100644 index 0000000..0593cae --- /dev/null +++ b/src/Meezi.Core/Entities/SupportTicketMessage.cs @@ -0,0 +1,13 @@ +using Meezi.Core.Enums; + +namespace Meezi.Core.Entities; + +public class SupportTicketMessage : BaseEntity +{ + public string TicketId { get; set; } = string.Empty; + public TicketMessageSenderKind SenderKind { get; set; } + public string SenderId { get; set; } = string.Empty; + public string Body { get; set; } = string.Empty; + + public SupportTicket? Ticket { get; set; } +} diff --git a/src/Meezi.Core/Entities/SystemAdmin.cs b/src/Meezi.Core/Entities/SystemAdmin.cs new file mode 100644 index 0000000..a8c1f69 --- /dev/null +++ b/src/Meezi.Core/Entities/SystemAdmin.cs @@ -0,0 +1,8 @@ +namespace Meezi.Core.Entities; + +public class SystemAdmin : BaseEntity +{ + public string Name { get; set; } = string.Empty; + public string Phone { get; set; } = string.Empty; + public bool IsActive { get; set; } = true; +} diff --git a/src/Meezi.Core/Entities/Table.cs b/src/Meezi.Core/Entities/Table.cs new file mode 100644 index 0000000..15f489e --- /dev/null +++ b/src/Meezi.Core/Entities/Table.cs @@ -0,0 +1,21 @@ +namespace Meezi.Core.Entities; + +public class Table : TenantEntity +{ + public string BranchId { get; set; } = string.Empty; + public string? SectionId { get; set; } + public string Number { get; set; } = string.Empty; + public int Capacity { get; set; } = 4; + public string? Floor { get; set; } + public int SortOrder { get; set; } + public string QrCode { get; set; } = string.Empty; + public string? ImageUrl { get; set; } + public string? VideoUrl { get; set; } + public bool IsActive { get; set; } = true; + public bool IsCleaning { get; set; } + + public Cafe Cafe { get; set; } = null!; + public Branch Branch { get; set; } = null!; + public TableSection? Section { get; set; } + public ICollection Orders { get; set; } = []; +} diff --git a/src/Meezi.Core/Entities/TableReservation.cs b/src/Meezi.Core/Entities/TableReservation.cs new file mode 100644 index 0000000..76af6c8 --- /dev/null +++ b/src/Meezi.Core/Entities/TableReservation.cs @@ -0,0 +1,21 @@ +using Meezi.Core.Enums; + +namespace Meezi.Core.Entities; + +public class TableReservation : BaseEntity +{ + public string CafeId { get; set; } = string.Empty; + public string? TableId { get; set; } + public string? CustomerId { get; set; } + public string GuestName { get; set; } = string.Empty; + public string GuestPhone { get; set; } = string.Empty; + public DateOnly Date { get; set; } + public TimeOnly Time { get; set; } + public int PartySize { get; set; } + public ReservationStatus Status { get; set; } = ReservationStatus.Pending; + public string? Notes { get; set; } + + public Cafe Cafe { get; set; } = null!; + public Table? Table { get; set; } + public Customer? Customer { get; set; } +} diff --git a/src/Meezi.Core/Entities/TableSection.cs b/src/Meezi.Core/Entities/TableSection.cs new file mode 100644 index 0000000..82768c1 --- /dev/null +++ b/src/Meezi.Core/Entities/TableSection.cs @@ -0,0 +1,13 @@ +namespace Meezi.Core.Entities; + +/// Floor area within a branch (سالن، تراس، VIP). +public class TableSection : TenantEntity +{ + public string BranchId { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public int SortOrder { get; set; } + public bool IsActive { get; set; } = true; + + public Branch Branch { get; set; } = null!; + public ICollection
Tables { get; set; } = []; +} diff --git a/src/Meezi.Core/Entities/Tax.cs b/src/Meezi.Core/Entities/Tax.cs new file mode 100644 index 0000000..3af7936 --- /dev/null +++ b/src/Meezi.Core/Entities/Tax.cs @@ -0,0 +1,13 @@ +namespace Meezi.Core.Entities; + +public class Tax : TenantEntity +{ + public string Name { get; set; } = string.Empty; + public decimal Rate { get; set; } + public bool IsDefault { get; set; } + public bool IsRequired { get; set; } + public bool IsCompound { get; set; } + + public Cafe Cafe { get; set; } = null!; + public ICollection MenuCategories { get; set; } = []; +} diff --git a/src/Meezi.Core/Entities/TopProductEntry.cs b/src/Meezi.Core/Entities/TopProductEntry.cs new file mode 100644 index 0000000..c03974c --- /dev/null +++ b/src/Meezi.Core/Entities/TopProductEntry.cs @@ -0,0 +1,9 @@ +namespace Meezi.Core.Entities; + +public class TopProductEntry +{ + public string ProductId { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public int Quantity { get; set; } + public decimal Revenue { get; set; } +} diff --git a/src/Meezi.Core/Entities/WebhookLog.cs b/src/Meezi.Core/Entities/WebhookLog.cs new file mode 100644 index 0000000..5535321 --- /dev/null +++ b/src/Meezi.Core/Entities/WebhookLog.cs @@ -0,0 +1,22 @@ +using Meezi.Core.Enums; + +namespace Meezi.Core.Entities; + +/// Audit log for inbound delivery platform webhooks (debugging & dead-letter). +public class WebhookLog : BaseEntity +{ + public string? CafeId { get; set; } + public DeliveryPlatform Platform { get; set; } + public string RawBody { get; set; } = string.Empty; + public string? SignatureHeader { get; set; } + public bool SignatureValid { get; set; } + public bool Processed { get; set; } + public bool Success { get; set; } + public string? ErrorMessage { get; set; } + public int AttemptCount { get; set; } + public string? ExternalOrderId { get; set; } + public string? MeeziOrderId { get; set; } + public DateTime? ProcessedAt { get; set; } + + public Cafe? Cafe { get; set; } +} diff --git a/src/Meezi.Core/Entities/WebsiteBlogPost.cs b/src/Meezi.Core/Entities/WebsiteBlogPost.cs new file mode 100644 index 0000000..7cf3be8 --- /dev/null +++ b/src/Meezi.Core/Entities/WebsiteBlogPost.cs @@ -0,0 +1,23 @@ +namespace Meezi.Core.Entities; + +public class WebsiteBlogPost : BaseEntity +{ + public string Slug { get; set; } = string.Empty; + public string TitleFa { get; set; } = string.Empty; + public string TitleEn { get; set; } = string.Empty; + public string ExcerptFa { get; set; } = string.Empty; + public string ExcerptEn { get; set; } = string.Empty; + public string ContentFa { get; set; } = string.Empty; // Markdown + public string ContentEn { get; set; } = string.Empty; + public string Author { get; set; } = "تیم میزی"; + public string CategoryFa { get; set; } = string.Empty; + public string CategoryEn { get; set; } = string.Empty; + public string TagsJson { get; set; } = "[]"; // JSON array of strings + public string? CoverImage { get; set; } + public bool IsPublished { get; set; } = false; + public DateTime? PublishedAt { get; set; } + public int ViewCount { get; set; } = 0; + + // Navigation + public ICollection Comments { get; set; } = []; +} diff --git a/src/Meezi.Core/Entities/WebsiteComment.cs b/src/Meezi.Core/Entities/WebsiteComment.cs new file mode 100644 index 0000000..87c3127 --- /dev/null +++ b/src/Meezi.Core/Entities/WebsiteComment.cs @@ -0,0 +1,14 @@ +namespace Meezi.Core.Entities; + +public class WebsiteComment : BaseEntity +{ + public string PostSlug { get; set; } = string.Empty; + public string AuthorName { get; set; } = string.Empty; + public string? AuthorEmail { get; set; } + public string Content { get; set; } = string.Empty; + public bool IsApproved { get; set; } = false; + public string? IpAddress { get; set; } + + // Navigation + public WebsiteBlogPost? Post { get; set; } +} diff --git a/src/Meezi.Core/Enums/CashTransactionType.cs b/src/Meezi.Core/Enums/CashTransactionType.cs new file mode 100644 index 0000000..3fd6d3d --- /dev/null +++ b/src/Meezi.Core/Enums/CashTransactionType.cs @@ -0,0 +1,10 @@ +namespace Meezi.Core.Enums; + +public enum CashTransactionType +{ + OrderPayment = 0, + Refund = 1, + Expense = 2, + Withdrawal = 3, + Deposit = 4 +} diff --git a/src/Meezi.Core/Enums/CouponType.cs b/src/Meezi.Core/Enums/CouponType.cs new file mode 100644 index 0000000..05a7d32 --- /dev/null +++ b/src/Meezi.Core/Enums/CouponType.cs @@ -0,0 +1,8 @@ +namespace Meezi.Core.Enums; + +public enum CouponType +{ + Percentage = 0, + FixedAmount = 1, + FreeItem = 2 +} diff --git a/src/Meezi.Core/Enums/CustomerGroup.cs b/src/Meezi.Core/Enums/CustomerGroup.cs new file mode 100644 index 0000000..6073373 --- /dev/null +++ b/src/Meezi.Core/Enums/CustomerGroup.cs @@ -0,0 +1,9 @@ +namespace Meezi.Core.Enums; + +public enum CustomerGroup +{ + Regular = 0, + Vip = 1, + New = 2, + Employee = 3 +} diff --git a/src/Meezi.Core/Enums/DeliveryPlatform.cs b/src/Meezi.Core/Enums/DeliveryPlatform.cs new file mode 100644 index 0000000..fdd34f2 --- /dev/null +++ b/src/Meezi.Core/Enums/DeliveryPlatform.cs @@ -0,0 +1,9 @@ +namespace Meezi.Core.Enums; + +public enum DeliveryPlatform +{ + Direct = 0, + Snappfood = 1, + Tap30 = 2, + Digikala = 3 +} diff --git a/src/Meezi.Core/Enums/DemoRequestStatus.cs b/src/Meezi.Core/Enums/DemoRequestStatus.cs new file mode 100644 index 0000000..2921590 --- /dev/null +++ b/src/Meezi.Core/Enums/DemoRequestStatus.cs @@ -0,0 +1,10 @@ +namespace Meezi.Core.Enums; + +public enum DemoRequestStatus +{ + New = 0, + Contacted = 1, + DemoScheduled = 2, + Converted = 3, + Rejected = 4 +} diff --git a/src/Meezi.Core/Enums/EmployeeRole.cs b/src/Meezi.Core/Enums/EmployeeRole.cs new file mode 100644 index 0000000..aa36782 --- /dev/null +++ b/src/Meezi.Core/Enums/EmployeeRole.cs @@ -0,0 +1,11 @@ +namespace Meezi.Core.Enums; + +public enum EmployeeRole +{ + Owner = 0, + Manager = 1, + Cashier = 2, + Waiter = 3, + Chef = 4, + Delivery = 5 +} diff --git a/src/Meezi.Core/Enums/ExpenseCategory.cs b/src/Meezi.Core/Enums/ExpenseCategory.cs new file mode 100644 index 0000000..17e7aa1 --- /dev/null +++ b/src/Meezi.Core/Enums/ExpenseCategory.cs @@ -0,0 +1,11 @@ +namespace Meezi.Core.Enums; + +public enum ExpenseCategory +{ + Supplies = 0, + Utilities = 1, + Salary = 2, + Rent = 3, + Maintenance = 4, + Other = 5 +} diff --git a/src/Meezi.Core/Enums/LeaveStatus.cs b/src/Meezi.Core/Enums/LeaveStatus.cs new file mode 100644 index 0000000..10bce16 --- /dev/null +++ b/src/Meezi.Core/Enums/LeaveStatus.cs @@ -0,0 +1,8 @@ +namespace Meezi.Core.Enums; + +public enum LeaveStatus +{ + Pending = 0, + Approved = 1, + Rejected = 2 +} diff --git a/src/Meezi.Core/Enums/OrderSource.cs b/src/Meezi.Core/Enums/OrderSource.cs new file mode 100644 index 0000000..db0d9fd --- /dev/null +++ b/src/Meezi.Core/Enums/OrderSource.cs @@ -0,0 +1,11 @@ +namespace Meezi.Core.Enums; + +public enum OrderSource +{ + Pos = 0, + GuestQr = 1, + Kiosk = 2, + SnappFood = 3, + Tap30 = 4, + Digikala = 5 +} diff --git a/src/Meezi.Core/Enums/OrderStatus.cs b/src/Meezi.Core/Enums/OrderStatus.cs new file mode 100644 index 0000000..4e48cee --- /dev/null +++ b/src/Meezi.Core/Enums/OrderStatus.cs @@ -0,0 +1,11 @@ +namespace Meezi.Core.Enums; + +public enum OrderStatus +{ + Pending = 0, + Confirmed = 1, + Preparing = 2, + Ready = 3, + Delivered = 4, + Cancelled = 5 +} diff --git a/src/Meezi.Core/Enums/OrderType.cs b/src/Meezi.Core/Enums/OrderType.cs new file mode 100644 index 0000000..32bc624 --- /dev/null +++ b/src/Meezi.Core/Enums/OrderType.cs @@ -0,0 +1,8 @@ +namespace Meezi.Core.Enums; + +public enum OrderType +{ + DineIn = 0, + Takeaway = 1, + Delivery = 2 +} diff --git a/src/Meezi.Core/Enums/PaymentMethod.cs b/src/Meezi.Core/Enums/PaymentMethod.cs new file mode 100644 index 0000000..36c53d5 --- /dev/null +++ b/src/Meezi.Core/Enums/PaymentMethod.cs @@ -0,0 +1,8 @@ +namespace Meezi.Core.Enums; + +public enum PaymentMethod +{ + Cash = 0, + Card = 1, + Credit = 2 +} diff --git a/src/Meezi.Core/Enums/PaymentProvider.cs b/src/Meezi.Core/Enums/PaymentProvider.cs new file mode 100644 index 0000000..ca180ad --- /dev/null +++ b/src/Meezi.Core/Enums/PaymentProvider.cs @@ -0,0 +1,31 @@ +namespace Meezi.Core.Enums; + +public enum PaymentProvider +{ + ZarinPal = 0, + Tara = 1, + SnappPay = 2 +} + +public static class PaymentProviderIds +{ + public const string ZarinPal = "zarinpal"; + public const string Tara = "tara"; + public const string SnappPay = "snapppay"; + + public static PaymentProvider? Parse(string? id) => id?.Trim().ToLowerInvariant() switch + { + ZarinPal => PaymentProvider.ZarinPal, + Tara => PaymentProvider.Tara, + SnappPay => PaymentProvider.SnappPay, + _ => null + }; + + public static string ToId(PaymentProvider provider) => provider switch + { + PaymentProvider.ZarinPal => ZarinPal, + PaymentProvider.Tara => Tara, + PaymentProvider.SnappPay => SnappPay, + _ => ZarinPal + }; +} diff --git a/src/Meezi.Core/Enums/PaymentStatus.cs b/src/Meezi.Core/Enums/PaymentStatus.cs new file mode 100644 index 0000000..a8cd881 --- /dev/null +++ b/src/Meezi.Core/Enums/PaymentStatus.cs @@ -0,0 +1,9 @@ +namespace Meezi.Core.Enums; + +public enum PaymentStatus +{ + Pending = 0, + Completed = 1, + Failed = 2, + Refunded = 3 +} diff --git a/src/Meezi.Core/Enums/PlanTier.cs b/src/Meezi.Core/Enums/PlanTier.cs new file mode 100644 index 0000000..ecbe4d9 --- /dev/null +++ b/src/Meezi.Core/Enums/PlanTier.cs @@ -0,0 +1,9 @@ +namespace Meezi.Core.Enums; + +public enum PlanTier +{ + Free = 0, + Pro = 1, + Business = 2, + Enterprise = 3 +} diff --git a/src/Meezi.Core/Enums/QueueTicketStatus.cs b/src/Meezi.Core/Enums/QueueTicketStatus.cs new file mode 100644 index 0000000..b08b23e --- /dev/null +++ b/src/Meezi.Core/Enums/QueueTicketStatus.cs @@ -0,0 +1,9 @@ +namespace Meezi.Core.Enums; + +public enum QueueTicketStatus +{ + Waiting = 0, + Called = 1, + Done = 2, + Cancelled = 3 +} diff --git a/src/Meezi.Core/Enums/ReservationStatus.cs b/src/Meezi.Core/Enums/ReservationStatus.cs new file mode 100644 index 0000000..77a3aa9 --- /dev/null +++ b/src/Meezi.Core/Enums/ReservationStatus.cs @@ -0,0 +1,12 @@ +namespace Meezi.Core.Enums; + +public enum ReservationStatus +{ + Pending = 0, + Confirmed = 1, + Cancelled = 2, + /// Guest arrived; order may be open at POS. + Seated = 3, + /// Paid and visit finished. + Completed = 4 +} diff --git a/src/Meezi.Core/Enums/ShiftStatus.cs b/src/Meezi.Core/Enums/ShiftStatus.cs new file mode 100644 index 0000000..2b5a94e --- /dev/null +++ b/src/Meezi.Core/Enums/ShiftStatus.cs @@ -0,0 +1,7 @@ +namespace Meezi.Core.Enums; + +public enum ShiftStatus +{ + Open = 0, + Closed = 1 +} diff --git a/src/Meezi.Core/Enums/ShiftType.cs b/src/Meezi.Core/Enums/ShiftType.cs new file mode 100644 index 0000000..21480bd --- /dev/null +++ b/src/Meezi.Core/Enums/ShiftType.cs @@ -0,0 +1,8 @@ +namespace Meezi.Core.Enums; + +public enum ShiftType +{ + Morning = 0, + Evening = 1, + DayOff = 2 +} diff --git a/src/Meezi.Core/Enums/StockMovementKind.cs b/src/Meezi.Core/Enums/StockMovementKind.cs new file mode 100644 index 0000000..825fd39 --- /dev/null +++ b/src/Meezi.Core/Enums/StockMovementKind.cs @@ -0,0 +1,9 @@ +namespace Meezi.Core.Enums; + +public enum StockMovementKind +{ + Manual = 0, + OrderDeduction = 1, + OrderRestore = 2, + Purchase = 3 +} diff --git a/src/Meezi.Core/Enums/SubscriptionPaymentStatus.cs b/src/Meezi.Core/Enums/SubscriptionPaymentStatus.cs new file mode 100644 index 0000000..03138cc --- /dev/null +++ b/src/Meezi.Core/Enums/SubscriptionPaymentStatus.cs @@ -0,0 +1,8 @@ +namespace Meezi.Core.Enums; + +public enum SubscriptionPaymentStatus +{ + Pending = 0, + Completed = 1, + Failed = 2 +} diff --git a/src/Meezi.Core/Enums/SupportTicketPriority.cs b/src/Meezi.Core/Enums/SupportTicketPriority.cs new file mode 100644 index 0000000..d346e5a --- /dev/null +++ b/src/Meezi.Core/Enums/SupportTicketPriority.cs @@ -0,0 +1,8 @@ +namespace Meezi.Core.Enums; + +public enum SupportTicketPriority +{ + Low = 0, + Normal = 1, + High = 2 +} diff --git a/src/Meezi.Core/Enums/SupportTicketStatus.cs b/src/Meezi.Core/Enums/SupportTicketStatus.cs new file mode 100644 index 0000000..fcad1f2 --- /dev/null +++ b/src/Meezi.Core/Enums/SupportTicketStatus.cs @@ -0,0 +1,10 @@ +namespace Meezi.Core.Enums; + +public enum SupportTicketStatus +{ + Open = 0, + InProgress = 1, + WaitingMerchant = 2, + Resolved = 3, + Closed = 4 +} diff --git a/src/Meezi.Core/Enums/TableBoardStatus.cs b/src/Meezi.Core/Enums/TableBoardStatus.cs new file mode 100644 index 0000000..bc8bba4 --- /dev/null +++ b/src/Meezi.Core/Enums/TableBoardStatus.cs @@ -0,0 +1,9 @@ +namespace Meezi.Core.Enums; + +public enum TableBoardStatus +{ + Free = 0, + Busy = 1, + Reserved = 2, + Cleaning = 3 +} diff --git a/src/Meezi.Core/Enums/TicketMessageSenderKind.cs b/src/Meezi.Core/Enums/TicketMessageSenderKind.cs new file mode 100644 index 0000000..f107667 --- /dev/null +++ b/src/Meezi.Core/Enums/TicketMessageSenderKind.cs @@ -0,0 +1,7 @@ +namespace Meezi.Core.Enums; + +public enum TicketMessageSenderKind +{ + Merchant = 0, + Admin = 1 +} diff --git a/src/Meezi.Core/Enums/UnifiedDeliveryStatus.cs b/src/Meezi.Core/Enums/UnifiedDeliveryStatus.cs new file mode 100644 index 0000000..50a8629 --- /dev/null +++ b/src/Meezi.Core/Enums/UnifiedDeliveryStatus.cs @@ -0,0 +1,12 @@ +namespace Meezi.Core.Enums; + +/// Platform-agnostic delivery order status before mapping to . +public enum UnifiedDeliveryStatus +{ + Pending = 0, + Confirmed = 1, + Preparing = 2, + Ready = 3, + Delivered = 4, + Cancelled = 5 +} diff --git a/src/Meezi.Core/Interfaces/IBranchContext.cs b/src/Meezi.Core/Interfaces/IBranchContext.cs new file mode 100644 index 0000000..8c6217c --- /dev/null +++ b/src/Meezi.Core/Interfaces/IBranchContext.cs @@ -0,0 +1,9 @@ +namespace Meezi.Core.Interfaces; + +/// Optional branch scope from JWT (staff terminal / POS session). +public interface IBranchContext +{ + string? CafeId { get; } + string? BranchId { get; } + bool HasBranch { get; } +} diff --git a/src/Meezi.Core/Interfaces/IPlatformRuntimeConfig.cs b/src/Meezi.Core/Interfaces/IPlatformRuntimeConfig.cs new file mode 100644 index 0000000..d0dc5de --- /dev/null +++ b/src/Meezi.Core/Interfaces/IPlatformRuntimeConfig.cs @@ -0,0 +1,8 @@ +namespace Meezi.Core.Interfaces; + +public interface IPlatformRuntimeConfig +{ + Task GetAsync(string key, CancellationToken cancellationToken = default); + Task> GetByPrefixAsync(string prefix, CancellationToken cancellationToken = default); + void InvalidateCache(); +} diff --git a/src/Meezi.Core/Interfaces/ISmsService.cs b/src/Meezi.Core/Interfaces/ISmsService.cs new file mode 100644 index 0000000..f9f037f --- /dev/null +++ b/src/Meezi.Core/Interfaces/ISmsService.cs @@ -0,0 +1,7 @@ +namespace Meezi.Core.Interfaces; + +public interface ISmsService +{ + Task SendOtpAsync(string phone, string otp, CancellationToken cancellationToken = default); + Task SendMessageAsync(string phone, string message, CancellationToken cancellationToken = default); +} diff --git a/src/Meezi.Core/Interfaces/ISnappPayGateway.cs b/src/Meezi.Core/Interfaces/ISnappPayGateway.cs new file mode 100644 index 0000000..5d33529 --- /dev/null +++ b/src/Meezi.Core/Interfaces/ISnappPayGateway.cs @@ -0,0 +1,18 @@ +namespace Meezi.Core.Interfaces; + +public record SnappPayInitResult(bool Success, string? PaymentToken, string? PaymentUrl, string? ErrorMessage); + +public record SnappPayVerifyResult(bool Success, string? RefId, string? ErrorMessage); + +public interface ISnappPayGateway +{ + Task IsEnabledAsync(CancellationToken cancellationToken = default); + Task RequestPaymentAsync( + long amountRials, + string transactionId, + string returnUrl, + CancellationToken cancellationToken = default); + Task VerifyAndSettleAsync( + string paymentToken, + CancellationToken cancellationToken = default); +} diff --git a/src/Meezi.Core/Interfaces/ISnappfoodClient.cs b/src/Meezi.Core/Interfaces/ISnappfoodClient.cs new file mode 100644 index 0000000..5773555 --- /dev/null +++ b/src/Meezi.Core/Interfaces/ISnappfoodClient.cs @@ -0,0 +1,11 @@ +namespace Meezi.Core.Interfaces; + +public interface ISnappfoodClient +{ + Task AcknowledgeOrderAsync(string snappfoodOrderId, CancellationToken cancellationToken = default); + Task NotifyOrderDeliveredAsync(string snappfoodOrderId, CancellationToken cancellationToken = default); + Task NotifyOrderStatusAsync( + string snappfoodOrderId, + string status, + CancellationToken cancellationToken = default); +} diff --git a/src/Meezi.Core/Interfaces/ITap30Client.cs b/src/Meezi.Core/Interfaces/ITap30Client.cs new file mode 100644 index 0000000..027c85f --- /dev/null +++ b/src/Meezi.Core/Interfaces/ITap30Client.cs @@ -0,0 +1,10 @@ +namespace Meezi.Core.Interfaces; + +public interface ITap30Client +{ + Task AcknowledgeOrderAsync(string tap30OrderId, CancellationToken cancellationToken = default); + Task NotifyOrderStatusAsync( + string tap30OrderId, + string status, + CancellationToken cancellationToken = default); +} diff --git a/src/Meezi.Core/Interfaces/ITaraPaymentGateway.cs b/src/Meezi.Core/Interfaces/ITaraPaymentGateway.cs new file mode 100644 index 0000000..673ad47 --- /dev/null +++ b/src/Meezi.Core/Interfaces/ITaraPaymentGateway.cs @@ -0,0 +1,18 @@ +namespace Meezi.Core.Interfaces; + +public record TaraInitResult(bool Success, string? TraceNumber, string? PaymentUrl, string? ErrorMessage); + +public record TaraVerifyResult(bool Success, string? RefId, string? ErrorMessage); + +public interface ITaraPaymentGateway +{ + Task IsEnabledAsync(CancellationToken cancellationToken = default); + Task RequestPaymentAsync( + long amountRials, + string invoiceNumber, + string callbackUrl, + CancellationToken cancellationToken = default); + Task VerifyPaymentAsync( + string traceNumber, + CancellationToken cancellationToken = default); +} diff --git a/src/Meezi.Core/Interfaces/ITarazTaxService.cs b/src/Meezi.Core/Interfaces/ITarazTaxService.cs new file mode 100644 index 0000000..8246a47 --- /dev/null +++ b/src/Meezi.Core/Interfaces/ITarazTaxService.cs @@ -0,0 +1,8 @@ +namespace Meezi.Core.Interfaces; + +public record TarazSubmitResult(bool Success, string? TrackingCode, string? Message); + +public interface ITarazTaxService +{ + Task SubmitDailyInvoicesAsync(string cafeId, DateTime dateUtc, CancellationToken cancellationToken = default); +} diff --git a/src/Meezi.Core/Interfaces/ITenantContext.cs b/src/Meezi.Core/Interfaces/ITenantContext.cs new file mode 100644 index 0000000..ff17fa7 --- /dev/null +++ b/src/Meezi.Core/Interfaces/ITenantContext.cs @@ -0,0 +1,17 @@ +using Meezi.Core.Enums; + +namespace Meezi.Core.Interfaces; + +public interface ITenantContext +{ + string? UserId { get; } + string? CafeId { get; } + EmployeeRole? Role { get; } + PlanTier? PlanTier { get; } + string? Language { get; } + /// Active branch from JWT when employee is branch-scoped. + string? BranchId { get; } + bool IsSystemAdmin { get; } + bool IsAuthenticated { get; } + bool IsCafeOwner => Role == EmployeeRole.Owner; +} diff --git a/src/Meezi.Core/Interfaces/IWebsiteService.cs b/src/Meezi.Core/Interfaces/IWebsiteService.cs new file mode 100644 index 0000000..7652c85 --- /dev/null +++ b/src/Meezi.Core/Interfaces/IWebsiteService.cs @@ -0,0 +1,14 @@ +namespace Meezi.Core.Interfaces; + +public interface IWebsiteService +{ + Task<(IReadOnlyList Posts, int Total)> GetPostsAsync( + string locale, int page, int limit, CancellationToken ct = default); + Task GetPostAsync(string slug, string locale, CancellationToken ct = default); + Task> GetCommentsAsync(string slug, CancellationToken ct = default); + Task AddCommentAsync(string slug, string authorName, string? email, + string content, string? ip, CancellationToken ct = default); + Task CreateDemoRequestAsync(string contactName, string businessName, + string phone, string? email, string branchCount, string? notes, string source, + CancellationToken ct = default); +} diff --git a/src/Meezi.Core/Interfaces/IZarinPalGateway.cs b/src/Meezi.Core/Interfaces/IZarinPalGateway.cs new file mode 100644 index 0000000..0b9049c --- /dev/null +++ b/src/Meezi.Core/Interfaces/IZarinPalGateway.cs @@ -0,0 +1,21 @@ +namespace Meezi.Core.Interfaces; + +public record ZarinPalRequestResult(bool Success, string? Authority, string? PaymentUrl, string? ErrorMessage); + +public record ZarinPalVerifyResult(bool Success, string? RefId, string? ErrorMessage); + +public interface IZarinPalGateway +{ + Task IsEnabledAsync(CancellationToken cancellationToken = default); + + Task RequestPaymentAsync( + long amountRials, + string description, + string callbackUrl, + CancellationToken cancellationToken = default); + + Task VerifyPaymentAsync( + string authority, + long amountRials, + CancellationToken cancellationToken = default); +} diff --git a/src/Meezi.Core/Meezi.Core.csproj b/src/Meezi.Core/Meezi.Core.csproj new file mode 100644 index 0000000..d36fa01 --- /dev/null +++ b/src/Meezi.Core/Meezi.Core.csproj @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/Meezi.Core/Platform/PlanLimitsData.cs b/src/Meezi.Core/Platform/PlanLimitsData.cs new file mode 100644 index 0000000..8b5ce7a --- /dev/null +++ b/src/Meezi.Core/Platform/PlanLimitsData.cs @@ -0,0 +1,43 @@ +namespace Meezi.Core.Platform; + +public class PlanLimitsData +{ + public int MaxOrdersPerDay { get; set; } = int.MaxValue; + public int MaxTerminals { get; set; } = int.MaxValue; + public int MaxCustomers { get; set; } = int.MaxValue; + public int MaxSmsPerMonth { get; set; } = int.MaxValue; + public int MaxBranches { get; set; } = int.MaxValue; + public int MaxReportHistoryDays { get; set; } = int.MaxValue; + + public static PlanLimitsData ForTier(Enums.PlanTier tier) => tier switch + { + Enums.PlanTier.Free => new PlanLimitsData + { + MaxOrdersPerDay = 50, + MaxTerminals = 1, + MaxCustomers = 50, + MaxSmsPerMonth = 0, + MaxBranches = 1, + MaxReportHistoryDays = 8 + }, + Enums.PlanTier.Pro => new PlanLimitsData + { + MaxOrdersPerDay = int.MaxValue, + MaxTerminals = 3, + MaxCustomers = int.MaxValue, + MaxSmsPerMonth = 50, + MaxBranches = 3, + MaxReportHistoryDays = 90 + }, + Enums.PlanTier.Business => new PlanLimitsData + { + MaxOrdersPerDay = int.MaxValue, + MaxTerminals = int.MaxValue, + MaxCustomers = int.MaxValue, + MaxSmsPerMonth = 200, + MaxBranches = int.MaxValue, + MaxReportHistoryDays = int.MaxValue + }, + _ => new PlanLimitsData() + }; +} diff --git a/src/Meezi.Core/Platform/PlatformIntegrationKeys.cs b/src/Meezi.Core/Platform/PlatformIntegrationKeys.cs new file mode 100644 index 0000000..81c2146 --- /dev/null +++ b/src/Meezi.Core/Platform/PlatformIntegrationKeys.cs @@ -0,0 +1,14 @@ +namespace Meezi.Core.Platform; + +/// PlatformSettings keys for third-party integrations (admin-configurable). +public static class PlatformIntegrationKeys +{ + public const string OpenAiApiKey = "integrations.openai.apiKey"; + public const string OpenAiEnabled = "integrations.openai.enabled"; + public const string OpenAiModel = "integrations.openai.model"; + public const string OpenAiCoffeeAdvisorEnabled = "integrations.openai.coffeeAdvisor.enabled"; + + public const string MeshyApiKey = "integrations.meshy.apiKey"; + public const string MeshyEnabled = "integrations.meshy.enabled"; + public const string MeshyMenu3dEnabled = "integrations.meshy.menu3d.enabled"; +} diff --git a/src/Meezi.Core/Utilities/OtpNormalizer.cs b/src/Meezi.Core/Utilities/OtpNormalizer.cs new file mode 100644 index 0000000..3556613 --- /dev/null +++ b/src/Meezi.Core/Utilities/OtpNormalizer.cs @@ -0,0 +1,38 @@ +namespace Meezi.Core.Utilities; + +/// Normalizes OTP input (Persian/Arabic digits, whitespace) to 6 ASCII digits. +public static class OtpNormalizer +{ + public static string Normalize(string? code) + { + if (string.IsNullOrWhiteSpace(code)) + return string.Empty; + + Span buffer = stackalloc char[code.Length]; + var n = 0; + foreach (var ch in code) + { + if (ch is >= '0' and <= '9') + buffer[n++] = ch; + else + { + var d = ch switch + { + >= '\u06F0' and <= '\u06F9' => ch - '\u06F0', // Persian + >= '\u0660' and <= '\u0669' => ch - '\u0660', // Arabic-Indic + _ => -1 + }; + if (d >= 0) + buffer[n++] = (char)('0' + d); + } + } + + return n == 0 ? string.Empty : new string(buffer[..n]); + } + + public static bool IsValidSixDigitCode(string? code) + { + var normalized = Normalize(code); + return normalized.Length == 6 && normalized.All(char.IsDigit); + } +} diff --git a/src/Meezi.Core/Utilities/PersianSearchNormalizer.cs b/src/Meezi.Core/Utilities/PersianSearchNormalizer.cs new file mode 100644 index 0000000..b348238 --- /dev/null +++ b/src/Meezi.Core/Utilities/PersianSearchNormalizer.cs @@ -0,0 +1,25 @@ +namespace Meezi.Core.Utilities; + +/// Lightweight normalization for Persian discover text search. +public static class PersianSearchNormalizer +{ + public static string Normalize(string? input) + { + if (string.IsNullOrWhiteSpace(input)) + return string.Empty; + + var s = input.Trim().ToLowerInvariant(); + s = s.Replace('ي', 'ی') + .Replace('ك', 'ک') + .Replace('\u200c', ' ') + .Replace('\u0640', ' ') + .Replace("آ", "ا") + .Replace("أ", "ا") + .Replace("إ", "ا"); + + while (s.Contains(" ", StringComparison.Ordinal)) + s = s.Replace(" ", " ", StringComparison.Ordinal); + + return s; + } +} diff --git a/src/Meezi.Core/Utilities/PhoneNormalizer.cs b/src/Meezi.Core/Utilities/PhoneNormalizer.cs new file mode 100644 index 0000000..7585b29 --- /dev/null +++ b/src/Meezi.Core/Utilities/PhoneNormalizer.cs @@ -0,0 +1,20 @@ +namespace Meezi.Core.Utilities; + +public static class PhoneNormalizer +{ + public static string Normalize(string phone) + { + var digits = new string(phone.Where(char.IsDigit).ToArray()); + + if (digits.StartsWith("98") && digits.Length == 12) + digits = "0" + digits[2..]; + + if (digits.Length == 10 && digits.StartsWith('9')) + digits = "0" + digits; + + return digits; + } + + public static bool IsValidIranMobile(string normalized) => + normalized.Length == 11 && normalized.StartsWith("09"); +} diff --git a/src/Meezi.Infrastructure/Branding/CafeThemeSerializer.cs b/src/Meezi.Infrastructure/Branding/CafeThemeSerializer.cs new file mode 100644 index 0000000..4b7fa84 --- /dev/null +++ b/src/Meezi.Infrastructure/Branding/CafeThemeSerializer.cs @@ -0,0 +1,153 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; +using Meezi.Core.Branding; + +namespace Meezi.Infrastructure.Branding; + +public static partial class CafeThemeSerializer +{ + private static readonly JsonSerializerOptions JsonOptions = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + PropertyNameCaseInsensitive = true + }; + + private static readonly HashSet ValidPalettes = + [ + "meezi-green", "ocean-blue", "royal-purple", "sunset-orange", "rose-blush", + "charcoal-gold", "espresso", "forest", "midnight", "coral", "gold-luxury", + "mint-fresh", "wine-bar", "slate-modern", "cherry", "teal-wave", "sand-cafe" + ]; + + private static readonly HashSet ValidPanelStyles = + [ + CafeThemeDefaults.PanelFlat, CafeThemeDefaults.PanelModern, CafeThemeDefaults.PanelGlass, + CafeThemeDefaults.PanelMinimal, CafeThemeDefaults.PanelBold, CafeThemeDefaults.PanelSoft, + CafeThemeDefaults.PanelElevated, CafeThemeDefaults.PanelOutline + ]; + + private static readonly HashSet ValidMenuStyles = + [ + CafeThemeDefaults.MenuCards, CafeThemeDefaults.MenuCompact, CafeThemeDefaults.MenuGrid, + CafeThemeDefaults.MenuList, CafeThemeDefaults.MenuMagazine, CafeThemeDefaults.MenuClassic + ]; + + private static readonly HashSet ValidMenuTextures = + [ + CafeThemeDefaults.MenuTextureNone, CafeThemeDefaults.MenuTexturePaper, + CafeThemeDefaults.MenuTextureLinen, CafeThemeDefaults.MenuTextureDots, + CafeThemeDefaults.MenuTextureGrid, CafeThemeDefaults.MenuTextureMarble, + CafeThemeDefaults.MenuTextureWood, CafeThemeDefaults.MenuTextureWarm + ]; + + private static readonly HashSet ValidDensities = + [ + CafeThemeDefaults.DensityCompact, CafeThemeDefaults.DensityComfortable, CafeThemeDefaults.DensitySpacious + ]; + + private static readonly HashSet ValidRadius = + [ + CafeThemeDefaults.RadiusNone, CafeThemeDefaults.RadiusSm, CafeThemeDefaults.RadiusMd, + CafeThemeDefaults.RadiusLg, CafeThemeDefaults.RadiusFull + ]; + + public static CafeTheme Parse(string? json) + { + if (string.IsNullOrWhiteSpace(json)) + return new CafeTheme(); + + try + { + var theme = JsonSerializer.Deserialize(json, JsonOptions) ?? new CafeTheme(); + return Normalize(theme); + } + catch + { + return new CafeTheme(); + } + } + + public static string Serialize(CafeTheme theme) => + JsonSerializer.Serialize(Normalize(theme), JsonOptions); + + public static CafeTheme Normalize(CafeTheme theme) + { + theme.PaletteId = ValidPalettes.Contains(theme.PaletteId) ? theme.PaletteId : CafeThemeDefaults.PaletteMeeziGreen; + theme.PanelStyle = ValidPanelStyles.Contains(theme.PanelStyle) ? theme.PanelStyle : CafeThemeDefaults.PanelModern; + theme.MenuStyle = ValidMenuStyles.Contains(theme.MenuStyle) ? theme.MenuStyle : CafeThemeDefaults.MenuCards; + theme.MenuTexture = ValidMenuTextures.Contains(theme.MenuTexture) + ? theme.MenuTexture + : CafeThemeDefaults.MenuTextureNone; + theme.Density = ValidDensities.Contains(theme.Density) ? theme.Density : CafeThemeDefaults.DensityComfortable; + theme.Radius = ValidRadius.Contains(theme.Radius) ? theme.Radius : CafeThemeDefaults.RadiusMd; + theme.Custom = NormalizeCustom(theme.Custom); + return theme; + } + + private static CafeThemeCustomColors? NormalizeCustom(CafeThemeCustomColors? custom) + { + if (custom is null) return null; + + var normalized = new CafeThemeCustomColors + { + Primary = NormalizeHex(custom.Primary), + Secondary = NormalizeHex(custom.Secondary), + Accent = NormalizeHex(custom.Accent), + Background = NormalizeHex(custom.Background), + Surface = NormalizeHex(custom.Surface), + Text = NormalizeHex(custom.Text), + TextMuted = NormalizeHex(custom.TextMuted), + Destructive = NormalizeHex(custom.Destructive), + Success = NormalizeHex(custom.Success), + PrimaryOpacity = NormalizeOpacity(custom.PrimaryOpacity), + SecondaryOpacity = NormalizeOpacity(custom.SecondaryOpacity), + AccentOpacity = NormalizeOpacity(custom.AccentOpacity), + BackgroundOpacity = NormalizeOpacity(custom.BackgroundOpacity), + SurfaceOpacity = NormalizeOpacity(custom.SurfaceOpacity), + TextOpacity = NormalizeOpacity(custom.TextOpacity), + TextMutedOpacity = NormalizeOpacity(custom.TextMutedOpacity), + DestructiveOpacity = NormalizeOpacity(custom.DestructiveOpacity), + SuccessOpacity = NormalizeOpacity(custom.SuccessOpacity) + }; + + return normalized.Primary is null + && normalized.Secondary is null + && normalized.Accent is null + && normalized.Background is null + && normalized.Surface is null + && normalized.Text is null + && normalized.TextMuted is null + && normalized.Destructive is null + && normalized.Success is null + && normalized.PrimaryOpacity is null + && normalized.SecondaryOpacity is null + && normalized.AccentOpacity is null + && normalized.BackgroundOpacity is null + && normalized.SurfaceOpacity is null + && normalized.TextOpacity is null + && normalized.TextMutedOpacity is null + && normalized.DestructiveOpacity is null + && normalized.SuccessOpacity is null + ? null + : normalized; + } + + private static string? NormalizeHex(string? value) + { + if (string.IsNullOrWhiteSpace(value)) return null; + var v = value.Trim(); + if (!HexColorRegex().IsMatch(v)) return null; + return v.StartsWith('#') ? v.ToUpperInvariant() : $"#{v.ToUpperInvariant()}"; + } + + private static int? NormalizeOpacity(int? value) + { + if (value is null) return null; + return Math.Clamp(value.Value, 0, 100); + } + + [GeneratedRegex(@"^#?[0-9A-Fa-f]{6}$", RegexOptions.Compiled)] + private static partial Regex HexColorRegex(); +} diff --git a/src/Meezi.Infrastructure/Data/AppDbContext.cs b/src/Meezi.Infrastructure/Data/AppDbContext.cs new file mode 100644 index 0000000..21b18cd --- /dev/null +++ b/src/Meezi.Infrastructure/Data/AppDbContext.cs @@ -0,0 +1,605 @@ +using System.Text.Json; +using Meezi.Core.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.ChangeTracking; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace Meezi.Infrastructure.Data; + +public class AppDbContext : DbContext +{ + public AppDbContext(DbContextOptions options) : base(options) + { + } + + public DbSet Cafes => Set(); + public DbSet Branches => Set(); + public DbSet
Tables => Set
(); + public DbSet TableSections => Set(); + public DbSet Employees => Set(); + public DbSet MenuCategories => Set(); + public DbSet MenuItems => Set(); + public DbSet BranchMenuItemOverrides => Set(); + public DbSet Orders => Set(); + public DbSet OrderItems => Set(); + public DbSet Payments => Set(); + public DbSet Customers => Set(); + public DbSet Coupons => Set(); + public DbSet Taxes => Set(); + public DbSet EmployeeSalaries => Set(); + public DbSet Attendances => Set(); + public DbSet EmployeeSchedules => Set(); + public DbSet RegisterShifts => Set(); + public DbSet CashTransactions => Set(); + public DbSet LeaveRequests => Set(); + public DbSet TableReservations => Set(); + public DbSet CafeReviews => Set(); + public DbSet CafeReviewPhotos => Set(); + public DbSet ConsumerAccounts => Set(); + public DbSet KitchenStations => Set(); + public DbSet SubscriptionPayments => Set(); + public DbSet Ingredients => Set(); + public DbSet MenuItemIngredients => Set(); + public DbSet StockMovements => Set(); + public DbSet QueueTickets => Set(); + public DbSet DailyReports => Set(); + public DbSet Expenses => Set(); + public DbSet WebhookLogs => Set(); + public DbSet DeliveryCommissionRates => Set(); + public DbSet SystemAdmins => Set(); + public DbSet PlatformPlanDefinitions => Set(); + public DbSet PlatformSettings => Set(); + public DbSet PlatformFeatures => Set(); + public DbSet CafeFeatureOverrides => Set(); + public DbSet SupportTickets => Set(); + public DbSet SupportTicketMessages => Set(); + public DbSet CafeNotifications => Set(); + + // Website CMS + public DbSet WebsiteBlogPosts => Set(); + public DbSet WebsiteComments => Set(); + public DbSet DemoRequests => Set(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity(e => + { + e.HasKey(x => x.Id); + e.HasIndex(x => x.Slug).IsUnique(); + e.Property(x => x.Name).HasMaxLength(200).IsRequired(); + e.Property(x => x.Slug).HasMaxLength(100).IsRequired(); + e.Property(x => x.SnappfoodVendorId).HasMaxLength(100); + e.Property(x => x.Tap30VendorId).HasMaxLength(100); + e.Property(x => x.DigikalaVendorId).HasMaxLength(100); + e.Property(x => x.ThemeJson).HasMaxLength(8000); + e.Property(x => x.DiscoverProfileJson).HasMaxLength(8000); + e.Property(x => x.DiscoverBadgesJson).HasMaxLength(2000); + e.Property(x => x.DefaultTaxRate).HasPrecision(5, 2); + e.HasQueryFilter(x => x.DeletedAt == null); + }); + + modelBuilder.Entity(e => + { + e.HasKey(x => x.Id); + e.Property(x => x.Name).HasMaxLength(200).IsRequired(); + e.Property(x => x.Address).HasMaxLength(500); + e.Property(x => x.City).HasMaxLength(100); + e.Property(x => x.Phone).HasMaxLength(20); + e.Property(x => x.ReceiptPrinterIp).HasMaxLength(45); + e.Property(x => x.KitchenPrinterIp).HasMaxLength(45); + e.Property(x => x.PosDeviceIp).HasMaxLength(45); + e.Property(x => x.ReceiptHeader).HasMaxLength(500); + e.Property(x => x.ReceiptFooter).HasMaxLength(500); + e.Property(x => x.WifiPassword).HasMaxLength(100); + e.HasIndex(x => new { x.CafeId, x.IsActive }); + e.HasOne(x => x.Cafe).WithMany(c => c.Branches).HasForeignKey(x => x.CafeId).OnDelete(DeleteBehavior.Cascade); + e.HasQueryFilter(x => x.DeletedAt == null); + }); + + modelBuilder.Entity(e => + { + e.HasKey(x => x.Id); + e.Property(x => x.Name).HasMaxLength(100).IsRequired(); + e.HasIndex(x => new { x.BranchId, x.Name }); + e.HasIndex(x => x.CafeId); + e.HasOne(x => x.Branch).WithMany(b => b.Sections).HasForeignKey(x => x.BranchId).OnDelete(DeleteBehavior.Cascade); + e.HasQueryFilter(x => x.DeletedAt == null); + }); + + modelBuilder.Entity
(e => + { + e.HasKey(x => x.Id); + e.Property(x => x.Number).HasMaxLength(50).IsRequired(); + e.Property(x => x.BranchId).IsRequired(); + e.Property(x => x.SortOrder).HasDefaultValue(0); + e.HasIndex(x => x.QrCode).IsUnique(); + e.HasIndex(x => new { x.BranchId, x.SectionId, x.SortOrder }); + e.HasOne(x => x.Cafe).WithMany(c => c.Tables).HasForeignKey(x => x.CafeId).OnDelete(DeleteBehavior.Cascade); + e.HasOne(x => x.Branch).WithMany(b => b.Tables).HasForeignKey(x => x.BranchId).OnDelete(DeleteBehavior.Restrict); + e.HasOne(x => x.Section).WithMany(s => s.Tables).HasForeignKey(x => x.SectionId).OnDelete(DeleteBehavior.SetNull); + e.HasQueryFilter(x => x.DeletedAt == null); + }); + + modelBuilder.Entity(e => + { + e.HasKey(x => x.Id); + e.HasIndex(x => new { x.CafeId, x.Phone }) + .IsUnique() + .HasFilter("\"DeletedAt\" IS NULL"); + e.HasIndex(x => x.BranchId); + e.HasOne(x => x.Cafe).WithMany(c => c.Employees).HasForeignKey(x => x.CafeId).OnDelete(DeleteBehavior.Cascade); + e.HasOne(x => x.Branch).WithMany(b => b.Staff).HasForeignKey(x => x.BranchId).OnDelete(DeleteBehavior.SetNull); + e.HasQueryFilter(x => x.DeletedAt == null); + }); + + modelBuilder.Entity(e => + { + e.HasKey(x => x.Id); + e.Property(x => x.Icon).HasMaxLength(32); + e.Property(x => x.IconPresetId).HasMaxLength(48); + e.Property(x => x.IconStyle).HasMaxLength(16); + e.Property(x => x.ImageUrl).HasMaxLength(500); + e.HasOne(x => x.Cafe).WithMany(c => c.MenuCategories).HasForeignKey(x => x.CafeId).OnDelete(DeleteBehavior.Cascade); + e.HasOne(x => x.Tax).WithMany(t => t.MenuCategories).HasForeignKey(x => x.TaxId).OnDelete(DeleteBehavior.SetNull); + e.HasOne(x => x.KitchenStation).WithMany(s => s.Categories).HasForeignKey(x => x.KitchenStationId).OnDelete(DeleteBehavior.SetNull); + e.HasQueryFilter(x => x.DeletedAt == null); + }); + + modelBuilder.Entity(e => + { + e.HasKey(x => x.Id); + e.Property(x => x.Model3dUrl).HasMaxLength(500); + e.Property(x => x.Price).HasPrecision(18, 2); + e.HasOne(x => x.Cafe).WithMany(c => c.MenuItems).HasForeignKey(x => x.CafeId).OnDelete(DeleteBehavior.Cascade); + e.HasOne(x => x.Category).WithMany(c => c.MenuItems).HasForeignKey(x => x.CategoryId).OnDelete(DeleteBehavior.Cascade); + e.HasQueryFilter(x => x.DeletedAt == null); + }); + + modelBuilder.Entity(e => + { + e.HasKey(x => x.Id); + e.Property(x => x.PriceOverride).HasPrecision(18, 2); + e.HasIndex(x => new { x.BranchId, x.MenuItemId }).IsUnique(); + e.HasIndex(x => x.CafeId); + e.HasOne(x => x.Branch).WithMany().HasForeignKey(x => x.BranchId).OnDelete(DeleteBehavior.Cascade); + e.HasOne(x => x.MenuItem).WithMany(m => m.BranchOverrides).HasForeignKey(x => x.MenuItemId).OnDelete(DeleteBehavior.Cascade); + e.HasQueryFilter(x => x.DeletedAt == null); + }); + + modelBuilder.Entity(e => + { + e.HasKey(x => x.Id); + e.Property(x => x.Subtotal).HasPrecision(18, 2); + e.Property(x => x.TaxTotal).HasPrecision(18, 2); + e.Property(x => x.Total).HasPrecision(18, 2); + e.Property(x => x.DiscountAmount).HasPrecision(18, 2); + e.Property(x => x.PlatformCommission).HasPrecision(18, 2); + e.Property(x => x.ExternalOrderId).HasMaxLength(120); + e.Property(x => x.DeliveryMetaJson).HasMaxLength(4000); + e.Property(x => x.GuestTrackingToken).HasMaxLength(64); + e.HasIndex(x => x.GuestTrackingToken); + e.HasIndex(x => new { x.CafeId, x.DisplayNumber }).IsUnique(); + e.HasIndex(x => new { x.CafeId, x.DeliveryPlatform, x.ExternalOrderId }); + e.HasOne(x => x.Cafe).WithMany(c => c.Orders).HasForeignKey(x => x.CafeId).OnDelete(DeleteBehavior.Cascade); + e.HasOne(x => x.Branch).WithMany(b => b.Orders).HasForeignKey(x => x.BranchId).OnDelete(DeleteBehavior.SetNull); + e.HasOne(x => x.Table).WithMany(t => t.Orders).HasForeignKey(x => x.TableId).OnDelete(DeleteBehavior.SetNull); + e.HasOne(x => x.Reservation).WithMany().HasForeignKey(x => x.ReservationId).OnDelete(DeleteBehavior.SetNull); + e.HasOne(x => x.Customer).WithMany(c => c.Orders).HasForeignKey(x => x.CustomerId).OnDelete(DeleteBehavior.SetNull); + e.HasOne(x => x.Employee).WithMany(emp => emp.Orders).HasForeignKey(x => x.EmployeeId).OnDelete(DeleteBehavior.SetNull); + e.HasOne(x => x.Coupon).WithMany(c => c.Orders).HasForeignKey(x => x.CouponId).OnDelete(DeleteBehavior.SetNull); + e.HasQueryFilter(x => x.DeletedAt == null); + }); + + modelBuilder.Entity(e => + { + e.HasKey(x => x.Id); + e.Property(x => x.UnitPrice).HasPrecision(18, 2); + e.HasOne(x => x.Order).WithMany(o => o.Items).HasForeignKey(x => x.OrderId).OnDelete(DeleteBehavior.Cascade); + e.HasOne(x => x.MenuItem).WithMany(m => m.OrderItems).HasForeignKey(x => x.MenuItemId).OnDelete(DeleteBehavior.Restrict); + e.HasQueryFilter(x => x.DeletedAt == null); + }); + + modelBuilder.Entity(e => + { + e.HasKey(x => x.Id); + e.Property(x => x.Amount).HasPrecision(18, 2); + e.HasOne(x => x.Order).WithMany(o => o.Payments).HasForeignKey(x => x.OrderId).OnDelete(DeleteBehavior.Cascade); + e.HasQueryFilter(x => x.DeletedAt == null); + }); + + modelBuilder.Entity(e => + { + e.HasKey(x => x.Id); + e.HasIndex(x => new { x.CafeId, x.Phone }); + e.HasOne(x => x.Cafe).WithMany(c => c.Customers).HasForeignKey(x => x.CafeId).OnDelete(DeleteBehavior.Cascade); + e.HasQueryFilter(x => x.DeletedAt == null); + }); + + modelBuilder.Entity(e => + { + e.HasKey(x => x.Id); + e.HasIndex(x => new { x.CafeId, x.Code }).IsUnique(); + e.Property(x => x.Value).HasPrecision(18, 2); + e.HasOne(x => x.Cafe).WithMany(c => c.Coupons).HasForeignKey(x => x.CafeId).OnDelete(DeleteBehavior.Cascade); + e.HasQueryFilter(x => x.DeletedAt == null); + }); + + modelBuilder.Entity(e => + { + e.HasKey(x => x.Id); + e.Property(x => x.Rate).HasPrecision(5, 2); + e.HasOne(x => x.Cafe).WithMany(c => c.Taxes).HasForeignKey(x => x.CafeId).OnDelete(DeleteBehavior.Cascade); + e.HasQueryFilter(x => x.DeletedAt == null); + }); + + modelBuilder.Entity(e => + { + e.HasKey(x => x.Id); + e.HasIndex(x => new { x.EmployeeId, x.MonthYear }).IsUnique(); + e.HasOne(x => x.Employee).WithMany(e => e.Salaries).HasForeignKey(x => x.EmployeeId).OnDelete(DeleteBehavior.Cascade); + e.HasQueryFilter(x => x.DeletedAt == null); + }); + + modelBuilder.Entity(e => + { + e.HasKey(x => x.Id); + e.HasIndex(x => new { x.EmployeeId, x.Date }).IsUnique(); + e.HasOne(x => x.Employee).WithMany(e => e.Attendances).HasForeignKey(x => x.EmployeeId).OnDelete(DeleteBehavior.Cascade); + e.HasQueryFilter(x => x.DeletedAt == null); + }); + + modelBuilder.Entity(e => + { + e.HasKey(x => x.Id); + e.ToTable("EmployeeSchedules"); + e.HasIndex(x => new { x.EmployeeId, x.DayOfWeek }).IsUnique(); + e.HasOne(x => x.Employee).WithMany(e => e.Schedules).HasForeignKey(x => x.EmployeeId).OnDelete(DeleteBehavior.Cascade); + e.HasQueryFilter(x => x.DeletedAt == null); + }); + + modelBuilder.Entity(e => + { + e.HasKey(x => x.Id); + e.ToTable("RegisterShifts"); + e.Property(x => x.OpeningCash).HasPrecision(18, 2); + e.Property(x => x.ClosingCash).HasPrecision(18, 2); + e.Property(x => x.ExpectedCash).HasPrecision(18, 2); + e.Property(x => x.Discrepancy).HasPrecision(18, 2); + e.HasIndex(x => new { x.BranchId, x.Status }); + e.HasOne(x => x.Cafe).WithMany().HasForeignKey(x => x.CafeId).OnDelete(DeleteBehavior.Cascade); + e.HasOne(x => x.Branch).WithMany().HasForeignKey(x => x.BranchId).OnDelete(DeleteBehavior.Cascade); + e.HasOne(x => x.OpenedBy).WithMany().HasForeignKey(x => x.OpenedByUserId).OnDelete(DeleteBehavior.Restrict); + e.HasOne(x => x.ClosedBy).WithMany().HasForeignKey(x => x.ClosedByUserId).OnDelete(DeleteBehavior.SetNull); + e.HasQueryFilter(x => x.DeletedAt == null); + }); + + modelBuilder.Entity(e => + { + e.HasKey(x => x.Id); + e.Property(x => x.Amount).HasPrecision(18, 2); + e.HasIndex(x => x.ShiftId); + e.HasIndex(x => new { x.CafeId, x.BranchId }); + e.HasOne(x => x.Shift).WithMany(s => s.Transactions).HasForeignKey(x => x.ShiftId).OnDelete(DeleteBehavior.Cascade); + e.HasOne(x => x.Branch).WithMany().HasForeignKey(x => x.BranchId).OnDelete(DeleteBehavior.SetNull); + e.HasQueryFilter(x => x.DeletedAt == null); + }); + + modelBuilder.Entity(e => + { + e.HasKey(x => x.Id); + e.HasOne(x => x.Employee).WithMany(e => e.LeaveRequests).HasForeignKey(x => x.EmployeeId).OnDelete(DeleteBehavior.Cascade); + e.HasQueryFilter(x => x.DeletedAt == null); + }); + + modelBuilder.Entity(e => + { + e.HasKey(x => x.Id); + e.HasIndex(x => new { x.CafeId, x.Date, x.Time }); + e.HasOne(x => x.Cafe).WithMany().HasForeignKey(x => x.CafeId).OnDelete(DeleteBehavior.Cascade); + e.HasOne(x => x.Table).WithMany().HasForeignKey(x => x.TableId).OnDelete(DeleteBehavior.SetNull); + e.HasOne(x => x.Customer).WithMany().HasForeignKey(x => x.CustomerId).OnDelete(DeleteBehavior.SetNull); + e.HasQueryFilter(x => x.DeletedAt == null); + }); + + modelBuilder.Entity(e => + { + e.HasKey(x => x.Id); + e.HasIndex(x => new { x.CafeId, x.CreatedAt }); + e.Property(x => x.AuthorName).HasMaxLength(200).IsRequired(); + e.HasOne(x => x.Cafe).WithMany(c => c.Reviews).HasForeignKey(x => x.CafeId).OnDelete(DeleteBehavior.Cascade); + e.HasQueryFilter(x => x.DeletedAt == null); + }); + + modelBuilder.Entity(e => + { + e.HasKey(x => x.Id); + e.Property(x => x.Url).HasMaxLength(500).IsRequired(); + e.HasIndex(x => x.ReviewId); + e.HasOne(x => x.Review).WithMany(r => r.Photos).HasForeignKey(x => x.ReviewId).OnDelete(DeleteBehavior.Cascade); + e.HasQueryFilter(x => x.DeletedAt == null); + }); + + modelBuilder.Entity(e => + { + e.HasKey(x => x.Id); + e.Property(x => x.Phone).HasMaxLength(20).IsRequired(); + e.HasIndex(x => x.Phone).IsUnique(); + e.Property(x => x.Name).HasMaxLength(200); + e.HasQueryFilter(x => x.DeletedAt == null); + }); + + modelBuilder.Entity(e => + { + e.HasKey(x => x.Id); + e.Property(x => x.Name).HasMaxLength(100).IsRequired(); + e.Property(x => x.PrinterIp).HasMaxLength(45); + e.HasIndex(x => new { x.CafeId, x.SortOrder }); + e.HasOne(x => x.Cafe).WithMany().HasForeignKey(x => x.CafeId).OnDelete(DeleteBehavior.Cascade); + e.HasOne(x => x.Branch).WithMany().HasForeignKey(x => x.BranchId).OnDelete(DeleteBehavior.SetNull); + e.HasQueryFilter(x => x.DeletedAt == null); + }); + + modelBuilder.Entity(e => + { + e.HasKey(x => x.Id); + e.HasIndex(x => x.Authority); + e.Property(x => x.AmountToman).HasPrecision(18, 2); + e.HasOne(x => x.Cafe).WithMany(c => c.SubscriptionPayments).HasForeignKey(x => x.CafeId).OnDelete(DeleteBehavior.Cascade); + e.HasQueryFilter(x => x.DeletedAt == null); + }); + + modelBuilder.Entity(e => + { + e.HasKey(x => x.Id); + e.Property(x => x.Name).HasMaxLength(200).IsRequired(); + e.Property(x => x.Unit).HasMaxLength(50).IsRequired(); + e.Property(x => x.QuantityOnHand).HasPrecision(18, 3); + e.Property(x => x.ReorderLevel).HasPrecision(18, 3); + e.Property(x => x.UnitCost).HasPrecision(18, 2); + e.Property(x => x.ParLevel).HasPrecision(18, 3); + e.Property(x => x.LowStockWarningPercent).HasPrecision(5, 2); + e.HasOne(x => x.Cafe).WithMany(c => c.Ingredients).HasForeignKey(x => x.CafeId).OnDelete(DeleteBehavior.Cascade); + e.HasQueryFilter(x => x.DeletedAt == null); + }); + + modelBuilder.Entity(e => + { + e.HasKey(x => x.Id); + e.Property(x => x.QuantityPerUnit).HasPrecision(18, 3); + e.HasIndex(x => new { x.CafeId, x.MenuItemId, x.IngredientId }).IsUnique(); + e.HasOne(x => x.MenuItem).WithMany(m => m.RecipeIngredients).HasForeignKey(x => x.MenuItemId) + .OnDelete(DeleteBehavior.Cascade); + e.HasOne(x => x.Ingredient).WithMany(i => i.MenuItemRecipes).HasForeignKey(x => x.IngredientId) + .OnDelete(DeleteBehavior.Cascade); + e.HasQueryFilter(x => x.DeletedAt == null); + }); + + modelBuilder.Entity(e => + { + e.HasKey(x => x.Id); + e.Property(x => x.Delta).HasPrecision(18, 3); + e.Property(x => x.TotalCostToman).HasPrecision(18, 2); + e.Property(x => x.ExpenseId).HasMaxLength(64); + e.Property(x => x.BranchId).HasMaxLength(64); + e.Property(x => x.Kind).HasConversion().HasMaxLength(30); + e.Property(x => x.OrderId).HasMaxLength(64); + e.HasIndex(x => new { x.CafeId, x.OrderId }); + e.HasOne(x => x.Cafe).WithMany().HasForeignKey(x => x.CafeId).OnDelete(DeleteBehavior.Cascade); + e.HasOne(x => x.Ingredient).WithMany(i => i.Movements).HasForeignKey(x => x.IngredientId).OnDelete(DeleteBehavior.Cascade); + e.HasQueryFilter(x => x.DeletedAt == null); + }); + + modelBuilder.Entity(e => + { + e.HasKey(x => x.Id); + e.HasIndex(x => new { x.CafeId, x.BranchId, x.ServiceDate, x.Number }).IsUnique(); + e.Property(x => x.CustomerLabel).HasMaxLength(200); + e.HasOne(x => x.Cafe).WithMany().HasForeignKey(x => x.CafeId).OnDelete(DeleteBehavior.Cascade); + e.HasOne(x => x.Branch).WithMany().HasForeignKey(x => x.BranchId).OnDelete(DeleteBehavior.SetNull); + e.HasOne(x => x.Order).WithMany().HasForeignKey(x => x.OrderId).OnDelete(DeleteBehavior.SetNull); + e.HasQueryFilter(x => x.DeletedAt == null); + }); + + modelBuilder.Entity(e => + { + e.HasKey(x => x.Id); + e.Property(x => x.Amount).HasPrecision(18, 2); + e.Property(x => x.Note).HasMaxLength(500); + e.Property(x => x.ReceiptImageUrl).HasMaxLength(500); + e.HasIndex(x => new { x.CafeId, x.BranchId, x.CreatedAt }); + e.HasOne(x => x.Branch).WithMany().HasForeignKey(x => x.BranchId).OnDelete(DeleteBehavior.Cascade); + e.HasOne(x => x.Shift).WithMany().HasForeignKey(x => x.ShiftId).OnDelete(DeleteBehavior.SetNull); + e.HasQueryFilter(x => x.DeletedAt == null); + }); + + modelBuilder.Entity(e => + { + e.HasKey(x => x.Id); + e.HasIndex(x => new { x.CafeId, x.BranchId, x.Date }).IsUnique(); + e.Property(x => x.TotalRevenue).HasPrecision(18, 2); + e.Property(x => x.CashRevenue).HasPrecision(18, 2); + e.Property(x => x.CardRevenue).HasPrecision(18, 2); + e.Property(x => x.CreditRevenue).HasPrecision(18, 2); + e.Property(x => x.AvgOrderValue).HasPrecision(18, 2); + e.Property(x => x.VoidAmount).HasPrecision(18, 2); + e.Property(x => x.TotalExpenses).HasPrecision(18, 2); + e.Property(x => x.NetIncome).HasPrecision(18, 2); + var topProductsConverter = new ValueConverter, string>( + v => JsonSerializer.Serialize(v, JsonSerializerOptions.Default), + v => JsonSerializer.Deserialize>(v, JsonSerializerOptions.Default) + ?? new List()); + var topProductsComparer = new ValueComparer>( + (a, b) => JsonSerializer.Serialize(a, JsonSerializerOptions.Default) + == JsonSerializer.Serialize(b, JsonSerializerOptions.Default), + v => JsonSerializer.Serialize(v, JsonSerializerOptions.Default).GetHashCode(), + v => JsonSerializer.Deserialize>( + JsonSerializer.Serialize(v, JsonSerializerOptions.Default), + JsonSerializerOptions.Default)!); + + e.Property(x => x.TopProducts) + .HasConversion(topProductsConverter, topProductsComparer) + .HasColumnType("jsonb"); + e.HasOne(x => x.Branch).WithMany().HasForeignKey(x => x.BranchId).OnDelete(DeleteBehavior.Cascade); + e.HasQueryFilter(x => x.DeletedAt == null); + }); + + modelBuilder.Entity(e => + { + e.HasKey(x => x.Id); + e.Property(x => x.RawBody).IsRequired(); + e.Property(x => x.SignatureHeader).HasMaxLength(256); + e.Property(x => x.ErrorMessage).HasMaxLength(2000); + e.Property(x => x.ExternalOrderId).HasMaxLength(120); + e.Property(x => x.MeeziOrderId).HasMaxLength(50); + e.HasIndex(x => new { x.Platform, x.CreatedAt }); + e.HasIndex(x => x.CafeId); + }); + + modelBuilder.Entity(e => + { + e.HasKey(x => x.Id); + e.Property(x => x.RatePercent).HasPrecision(5, 2); + e.HasIndex(x => new { x.CafeId, x.Platform }).IsUnique(); + e.HasOne(x => x.Cafe).WithMany().HasForeignKey(x => x.CafeId).OnDelete(DeleteBehavior.Cascade); + e.HasQueryFilter(x => x.DeletedAt == null); + }); + + modelBuilder.Entity(e => + { + e.HasKey(x => x.Id); + e.Property(x => x.Name).HasMaxLength(200).IsRequired(); + e.Property(x => x.Phone).HasMaxLength(20).IsRequired(); + e.HasIndex(x => x.Phone).IsUnique(); + e.HasQueryFilter(x => x.DeletedAt == null); + }); + + modelBuilder.Entity(e => + { + e.HasKey(x => x.Id); + e.Property(x => x.DisplayNameFa).HasMaxLength(200).IsRequired(); + e.Property(x => x.DisplayNameEn).HasMaxLength(200); + e.Property(x => x.MonthlyPriceToman).HasPrecision(18, 0); + e.Property(x => x.LimitsJson).HasMaxLength(4000).IsRequired(); + e.Property(x => x.FeaturesJson).HasMaxLength(4000); + e.HasIndex(x => x.Tier).IsUnique(); + e.HasQueryFilter(x => x.DeletedAt == null); + }); + + modelBuilder.Entity(e => + { + e.HasKey(x => x.Id); + e.Property(x => x.Key).HasMaxLength(120).IsRequired(); + e.Property(x => x.Value).HasMaxLength(8000).IsRequired(); + e.Property(x => x.Category).HasMaxLength(60).IsRequired(); + e.Property(x => x.DescriptionFa).HasMaxLength(500); + e.HasIndex(x => x.Key).IsUnique(); + e.HasQueryFilter(x => x.DeletedAt == null); + }); + + modelBuilder.Entity(e => + { + e.HasKey(x => x.Id); + e.Property(x => x.Key).HasMaxLength(80).IsRequired(); + e.Property(x => x.DisplayNameFa).HasMaxLength(200).IsRequired(); + e.Property(x => x.DisplayNameEn).HasMaxLength(200); + e.Property(x => x.ModuleGroup).HasMaxLength(60).IsRequired(); + e.HasIndex(x => x.Key).IsUnique(); + e.HasQueryFilter(x => x.DeletedAt == null); + }); + + modelBuilder.Entity(e => + { + e.HasKey(x => x.Id); + e.Property(x => x.FeatureKey).HasMaxLength(80).IsRequired(); + e.HasIndex(x => new { x.CafeId, x.FeatureKey }).IsUnique(); + e.HasOne().WithMany().HasForeignKey(x => x.CafeId).OnDelete(DeleteBehavior.Cascade); + e.HasQueryFilter(x => x.DeletedAt == null); + }); + + modelBuilder.Entity(e => + { + e.HasKey(x => x.Id); + e.Property(x => x.Subject).HasMaxLength(300).IsRequired(); + e.HasIndex(x => new { x.CafeId, x.Status, x.UpdatedAt }); + e.HasOne(x => x.Cafe).WithMany().HasForeignKey(x => x.CafeId).OnDelete(DeleteBehavior.Cascade); + e.HasOne(x => x.CreatedByEmployee).WithMany().HasForeignKey(x => x.CreatedByEmployeeId) + .OnDelete(DeleteBehavior.Restrict); + e.HasQueryFilter(x => x.DeletedAt == null); + }); + + modelBuilder.Entity(e => + { + e.HasKey(x => x.Id); + e.Property(x => x.Body).HasMaxLength(8000).IsRequired(); + e.HasIndex(x => new { x.TicketId, x.CreatedAt }); + e.HasOne(x => x.Ticket).WithMany(t => t.Messages).HasForeignKey(x => x.TicketId) + .OnDelete(DeleteBehavior.Cascade); + e.HasQueryFilter(x => x.DeletedAt == null); + }); + + modelBuilder.Entity(e => + { + e.HasKey(x => x.Id); + e.Property(x => x.Type).HasMaxLength(60).IsRequired(); + e.Property(x => x.Title).HasMaxLength(300).IsRequired(); + e.Property(x => x.Body).HasMaxLength(1000); + e.Property(x => x.ReferenceId).HasMaxLength(64); + e.Property(x => x.TableNumber).HasMaxLength(40); + e.HasIndex(x => new { x.CafeId, x.IsRead, x.CreatedAt }); + e.HasQueryFilter(x => x.DeletedAt == null); + }); + + // ── Website CMS ────────────────────────────────────────────────────── + modelBuilder.Entity(e => + { + e.HasKey(x => x.Id); + e.Property(x => x.Slug).HasMaxLength(200).IsRequired(); + e.Property(x => x.TitleFa).HasMaxLength(400).IsRequired(); + e.Property(x => x.TitleEn).HasMaxLength(400); + e.Property(x => x.ExcerptFa).HasMaxLength(1000); + e.Property(x => x.ExcerptEn).HasMaxLength(1000); + e.Property(x => x.ContentFa).HasColumnType("text"); + e.Property(x => x.ContentEn).HasColumnType("text"); + e.Property(x => x.Author).HasMaxLength(200); + e.Property(x => x.CategoryFa).HasMaxLength(100); + e.Property(x => x.CategoryEn).HasMaxLength(100); + e.Property(x => x.TagsJson).HasMaxLength(2000).HasDefaultValue("[]"); + e.Property(x => x.CoverImage).HasMaxLength(500); + e.HasIndex(x => x.Slug).IsUnique(); + e.HasIndex(x => new { x.IsPublished, x.PublishedAt }); + e.HasMany(x => x.Comments).WithOne(c => c.Post).HasForeignKey(c => c.PostSlug) + .HasPrincipalKey(p => p.Slug).OnDelete(DeleteBehavior.Cascade); + e.HasQueryFilter(x => x.DeletedAt == null); + }); + + modelBuilder.Entity(e => + { + e.HasKey(x => x.Id); + e.Property(x => x.PostSlug).HasMaxLength(200).IsRequired(); + e.Property(x => x.AuthorName).HasMaxLength(100).IsRequired(); + e.Property(x => x.AuthorEmail).HasMaxLength(200); + e.Property(x => x.Content).HasMaxLength(3000).IsRequired(); + e.Property(x => x.IpAddress).HasMaxLength(50); + e.HasIndex(x => new { x.PostSlug, x.IsApproved, x.CreatedAt }); + e.HasQueryFilter(x => x.DeletedAt == null); + }); + + modelBuilder.Entity(e => + { + e.HasKey(x => x.Id); + e.Property(x => x.ContactName).HasMaxLength(200).IsRequired(); + e.Property(x => x.BusinessName).HasMaxLength(300).IsRequired(); + e.Property(x => x.Phone).HasMaxLength(20).IsRequired(); + e.Property(x => x.Email).HasMaxLength(200); + e.Property(x => x.BranchCount).HasMaxLength(20); + e.Property(x => x.Notes).HasMaxLength(2000); + e.Property(x => x.Source).HasMaxLength(50).HasDefaultValue("website"); + e.Property(x => x.AdminNotes).HasMaxLength(2000); + e.HasIndex(x => new { x.Status, x.CreatedAt }); + e.HasQueryFilter(x => x.DeletedAt == null); + }); + + } +} diff --git a/src/Meezi.Infrastructure/Data/BranchContext.cs b/src/Meezi.Infrastructure/Data/BranchContext.cs new file mode 100644 index 0000000..9a3ef17 --- /dev/null +++ b/src/Meezi.Infrastructure/Data/BranchContext.cs @@ -0,0 +1,10 @@ +using Meezi.Core.Interfaces; + +namespace Meezi.Infrastructure.Data; + +public class BranchContext : IBranchContext +{ + public string? CafeId { get; set; } + public string? BranchId { get; set; } + public bool HasBranch => !string.IsNullOrEmpty(BranchId); +} diff --git a/src/Meezi.Infrastructure/Data/DatabaseSchemaPatches.cs b/src/Meezi.Infrastructure/Data/DatabaseSchemaPatches.cs new file mode 100644 index 0000000..eb532fe --- /dev/null +++ b/src/Meezi.Infrastructure/Data/DatabaseSchemaPatches.cs @@ -0,0 +1,18 @@ +using Microsoft.EntityFrameworkCore; + +namespace Meezi.Infrastructure.Data; + +/// Idempotent SQL fixes for databases where EF history and schema diverged. +public static class DatabaseSchemaPatches +{ + public static async Task ApplyAsync(AppDbContext db, CancellationToken cancellationToken = default) + { + await db.Database.ExecuteSqlRawAsync( + """ + ALTER TABLE "OrderItems" ADD COLUMN IF NOT EXISTS "IsVoided" boolean NOT NULL DEFAULT false; + ALTER TABLE "OrderItems" ADD COLUMN IF NOT EXISTS "VoidedAt" timestamp with time zone; + ALTER TABLE "OrderItems" ADD COLUMN IF NOT EXISTS "VoidedByUserId" text; + """, + cancellationToken); + } +} diff --git a/src/Meezi.Infrastructure/Data/DemoCouponSeeder.cs b/src/Meezi.Infrastructure/Data/DemoCouponSeeder.cs new file mode 100644 index 0000000..78316ba --- /dev/null +++ b/src/Meezi.Infrastructure/Data/DemoCouponSeeder.cs @@ -0,0 +1,43 @@ +using Meezi.Core.Entities; +using Meezi.Core.Enums; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace Meezi.Infrastructure.Data; + +public static class DemoCouponSeeder +{ + public static async Task EnsureCouponsAsync(AppDbContext db, string cafeId, ILogger logger) + { + if (await db.Coupons.AnyAsync(c => c.CafeId == cafeId && c.Code == "WELCOME10")) + return; + + db.Coupons.AddRange( + new Coupon + { + Id = "coupon_demo_welcome10", + CafeId = cafeId, + Code = "WELCOME10", + Type = CouponType.Percentage, + Value = 10, + MaxDiscount = 50_000, + MinOrderAmount = 100_000, + UsageLimit = 100, + IsActive = true + }, + new Coupon + { + Id = "coupon_demo_save20k", + CafeId = cafeId, + Code = "SAVE20", + Type = CouponType.FixedAmount, + Value = 20_000, + MinOrderAmount = 150_000, + UsageLimit = 50, + IsActive = true + }); + + await db.SaveChangesAsync(); + logger.LogInformation("Demo coupons seeded: WELCOME10, SAVE20 for cafe {CafeId}", cafeId); + } +} diff --git a/src/Meezi.Infrastructure/Data/DemoEmployeesCatalog.cs b/src/Meezi.Infrastructure/Data/DemoEmployeesCatalog.cs new file mode 100644 index 0000000..8ac2a0f --- /dev/null +++ b/src/Meezi.Infrastructure/Data/DemoEmployeesCatalog.cs @@ -0,0 +1,28 @@ +using Meezi.Core.Enums; + +namespace Meezi.Infrastructure.Data; + +/// Demo staff for development OTP login (see data/demo-credentials.json). +public static class DemoEmployeesCatalog +{ + public const string DefaultBranchId = "branch_demo_main"; + + public sealed record EmployeeSeed( + string Id, + string Name, + string Phone, + EmployeeRole Role, + string BranchId = DefaultBranchId, + decimal BaseSalary = 0); + + public static IReadOnlyList Employees { get; } = + [ + new("emp_demo_owner", "مدیر دمو", "09121234567", EmployeeRole.Owner), + new("emp_demo_manager", "مدیر شعبه", "09121111111", EmployeeRole.Manager), + new("emp_demo_cashier", "صندوقدار", "09122222222", EmployeeRole.Cashier), + new("emp_demo_waiter", "گارسون", "09123333333", EmployeeRole.Waiter), + new("emp_demo_waiter2", "گارسون ۲", "09124444444", EmployeeRole.Waiter), + new("emp_demo_chef", "آشپز", "09125555555", EmployeeRole.Chef), + new("emp_demo_delivery", "پیک", "09126666666", EmployeeRole.Delivery), + ]; +} diff --git a/src/Meezi.Infrastructure/Data/DemoEmployeesSeeder.cs b/src/Meezi.Infrastructure/Data/DemoEmployeesSeeder.cs new file mode 100644 index 0000000..7ca34dc --- /dev/null +++ b/src/Meezi.Infrastructure/Data/DemoEmployeesSeeder.cs @@ -0,0 +1,44 @@ +using Meezi.Core.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace Meezi.Infrastructure.Data; + +public static class DemoEmployeesSeeder +{ + public static async Task EnsureEmployeesAsync(AppDbContext db, string cafeId, ILogger logger) + { + var existingPhones = await db.Employees + .Where(e => e.CafeId == cafeId && e.DeletedAt == null) + .Select(e => e.Phone) + .ToListAsync(); + + var added = 0; + foreach (var seed in DemoEmployeesCatalog.Employees) + { + if (existingPhones.Contains(seed.Phone)) + continue; + + if (await db.Employees.AnyAsync(e => e.Id == seed.Id, cancellationToken: default)) + continue; + + db.Employees.Add(new Employee + { + Id = seed.Id, + CafeId = cafeId, + BranchId = seed.BranchId, + Name = seed.Name, + Phone = seed.Phone, + Role = seed.Role, + BaseSalary = seed.BaseSalary + }); + added++; + } + + if (added > 0) + { + await db.SaveChangesAsync(); + logger.LogInformation("Demo employees seed: {Count} added for cafe {CafeId}", added, cafeId); + } + } +} diff --git a/src/Meezi.Infrastructure/Data/DemoMenuCatalog.cs b/src/Meezi.Infrastructure/Data/DemoMenuCatalog.cs new file mode 100644 index 0000000..1b04b94 --- /dev/null +++ b/src/Meezi.Infrastructure/Data/DemoMenuCatalog.cs @@ -0,0 +1,149 @@ +namespace Meezi.Infrastructure.Data; + +/// +/// Café demo menu aligned with Kaggle Food-101 class names (see Food101ImageFallbacks + data/menu-image-manifest.json). +/// +public static class DemoMenuCatalog +{ + public sealed record CategorySeed( + string Id, + string Name, + string NameEn, + string? NameAr, + int SortOrder, + string? Icon = null, + string? IconPresetId = null, + string? IconStyle = "flat"); + + public sealed record ItemSeed( + string Id, + string CategoryId, + string Name, + string NameEn, + string? NameAr, + string? Description, + decimal PriceToman, + decimal DiscountPercent, + string Food101Class); + + private static string Img(string itemId, string food101Class) + { + var kind = DemoMenuCategoryKinds.KindFor(itemId, food101Class); + var local = MenuImageManifest.GetLocalImageOverride(itemId); + if (!string.IsNullOrWhiteSpace(local)) + return local; + return Food101ImageFallbacks.Resolve(food101Class, kind); + } + + public static IReadOnlyList Categories { get; } = + [ + new("cat_demo_drinks", "نوشیدنی گرم", "Hot drinks", "مشروبات ساخنة", 1, IconPresetId: "drinks-hot"), + new("cat_demo_cold", "نوشیدنی سرد", "Cold drinks", "مشروبات باردة", 2, IconPresetId: "drinks-cold"), + new("cat_demo_breakfast", "صبحانه", "Breakfast", "فطور", 3, IconPresetId: "breakfast"), + new("cat_demo_food", "غذا و پیش‌غذا", "Food & snacks", "طعام", 4, IconPresetId: "food-mains"), + new("cat_demo_pasta", "پاستا و پیتزا", "Pasta & pizza", "معكرونة وبيتزا", 5, IconPresetId: "pasta-pizza"), + new("cat_demo_dessert", "دسر", "Desserts", "حلويات", 6, IconPresetId: "dessert"), + ]; + + public static IReadOnlyList Items { get; } = + [ + // Hot drinks + new("item_demo_espresso", "cat_demo_drinks", "اسپرسو", "Espresso", "إسبريسو", "دوبل یا سینگل", 65_000, 0, "espresso"), + new("item_demo_americano", "cat_demo_drinks", "آمریکانو", "Americano", "أمريكانو", null, 75_000, 0, "cappuccino"), + new("item_demo_latte", "cat_demo_drinks", "لاته", "Latte", "لاتيه", "شیر بخار گرفته", 120_000, 0, "latte"), + new("item_demo_cappuccino", "cat_demo_drinks", "کاپوچینو", "Cappuccino", "كابتشينو", null, 110_000, 10, "cappuccino"), + new("item_demo_mocha", "cat_demo_drinks", "موکا", "Mocha", "موكا", "شکلات و قهوه", 135_000, 0, "mocha"), + new("item_demo_tea", "cat_demo_drinks", "چای ماسالا", "Masala tea", "شاي ماسالا", null, 85_000, 0, "miso_soup"), + + // Cold drinks + new("item_demo_iced_latte", "cat_demo_cold", "آیس لاته", "Iced latte", "آيس لاتيه", null, 130_000, 0, "iced_coffee"), + new("item_demo_cold_brew", "cat_demo_cold", "کولد برو", "Cold brew", "كولد برو", null, 140_000, 0, "iced_coffee"), + new("item_demo_lemonade", "cat_demo_cold", "لیموناد", "Lemonade", "ليمونادة", "تازه", 95_000, 0, "lemonade"), + new("item_demo_smoothie", "cat_demo_cold", "اسموتی توت", "Berry smoothie", "سموذي", null, 150_000, 15, "smoothie"), + + // Breakfast + new("item_demo_croissant", "cat_demo_breakfast", "کروسان", "Croissant", "كرواسان", "کره‌ای", 75_000, 0, "croque_madame"), + new("item_demo_omelette", "cat_demo_breakfast", "املت", "Omelette", "أومليت", "نان سنگک", 145_000, 0, "omelette"), + new("item_demo_avocado", "cat_demo_breakfast", "توست آووکادو", "Avocado toast", "توست أفوكادو", null, 185_000, 0, "avocado_toast"), + new("item_demo_pancakes", "cat_demo_breakfast", "پنکیک", "Pancakes", "فطائر", "عسل و کره", 165_000, 0, "pancakes"), + new("item_demo_waffles", "cat_demo_breakfast", "وافل", "Waffles", "وافل", null, 175_000, 0, "waffles"), + new("item_demo_french_toast", "cat_demo_breakfast", "فرنچ تست", "French toast", "توست فرنسي", null, 155_000, 0, "french_toast"), + new("item_demo_eggs_benedict", "cat_demo_breakfast", "اگ بندیکت", "Eggs Benedict", "بيض بنديكت", null, 195_000, 0, "eggs_benedict"), + + // Food & snacks + new("item_demo_sandwich", "cat_demo_food", "ساندویچ مرغ", "Chicken sandwich", "ساندويتش دجاج", null, 195_000, 0, "club_sandwich"), + new("item_demo_salad", "cat_demo_food", "سالاد سزار", "Caesar salad", "سلطة سيزر", null, 175_000, 0, "caesar_salad"), + new("item_demo_greek_salad", "cat_demo_food", "سالاد یونانی", "Greek salad", "سلطة يونانية", null, 168_000, 0, "greek_salad"), + new("item_demo_burger", "cat_demo_food", "همبرگر", "Burger", "برجر", "۱۵۰ گرم گوشت", 245_000, 0, "hamburger"), + new("item_demo_steak", "cat_demo_food", "استیک", "Steak", "ستيك", "medium", 385_000, 0, "steak"), + new("item_demo_salmon", "cat_demo_food", "سالمون گریل", "Grilled salmon", "سلمون مشوي", null, 320_000, 0, "grilled_salmon"), + new("item_demo_tacos", "cat_demo_food", "تاکو", "Tacos", "تاكو", "سه عدد", 210_000, 0, "tacos"), + new("item_demo_shawarma", "cat_demo_food", "شاورما", "Shawarma", "شاورما", null, 185_000, 0, "shawarma"), + new("item_demo_falafel", "cat_demo_food", "فلافل", "Falafel", "فلافل", "۶ عدد", 125_000, 0, "falafel"), + new("item_demo_hummus", "cat_demo_food", "حمص", "Hummus", "حمص", "نان پیتا", 95_000, 0, "hummus"), + new("item_demo_fries", "cat_demo_food", "سیب‌زمینی سرخ‌کرده", "French fries", "بطاطس مقلية", null, 85_000, 0, "french_fries"), + new("item_demo_spring_rolls", "cat_demo_food", "اسپرینگ رول", "Spring rolls", "سبرينغ رول", null, 115_000, 0, "spring_rolls"), + new("item_demo_ramen", "cat_demo_food", "رامن", "Ramen", "رامن", null, 235_000, 0, "ramen"), + new("item_demo_pho", "cat_demo_food", "فو", "Pho", "فو", null, 225_000, 0, "pho"), + new("item_demo_sushi", "cat_demo_food", "سوشی", "Sushi", "سوشي", "۸ تکه", 290_000, 0, "sushi"), + + // Pasta & pizza + new("item_demo_pasta", "cat_demo_pasta", "پاستا آلفردو", "Alfredo pasta", "باستا", null, 220_000, 0, "pasta_carbonara"), + new("item_demo_carbonara", "cat_demo_pasta", "کاربونارا", "Carbonara", "كاربونارا", null, 228_000, 0, "spaghetti_carbonara"), + new("item_demo_bolognese", "cat_demo_pasta", "بولونز", "Bolognese", "بولونيز", null, 215_000, 0, "spaghetti_bolognese"), + new("item_demo_lasagna", "cat_demo_pasta", "لازانیا", "Lasagna", "لازانيا", null, 240_000, 0, "lasagna"), + new("item_demo_gnocchi", "cat_demo_pasta", "نیوکی", "Gnocchi", "جنوكي", null, 232_000, 0, "gnocchi"), + new("item_demo_pizza", "cat_demo_pasta", "پیتزا مارگاریتا", "Margherita pizza", "بيتزا", null, 265_000, 10, "pizza"), + new("item_demo_risotto", "cat_demo_pasta", "ریزوتو قارچ", "Mushroom risotto", "ريزوتو", null, 238_000, 0, "mushroom_risotto"), + + // Desserts + new("item_demo_cake", "cat_demo_dessert", "کیک شکلاتی", "Chocolate cake", "كيك شوكولاتة", "برشی", 95_000, 15, "chocolate_cake"), + new("item_demo_cheesecake", "cat_demo_dessert", "چیزکیک", "Cheesecake", "تشيز كيك", null, 115_000, 0, "cheesecake"), + new("item_demo_brownie", "cat_demo_dessert", "براونی", "Brownie", "براوني", "بستنی وانیلی", 105_000, 0, "brownie"), + new("item_demo_icecream", "cat_demo_dessert", "بستنی", "Ice cream", "آيس كريم", "دو اسکوپ", 88_000, 0, "ice_cream"), + new("item_demo_tiramisu", "cat_demo_dessert", "تیرامیسو", "Tiramisu", "تيراميسو", null, 125_000, 0, "tiramisu"), + new("item_demo_donuts", "cat_demo_dessert", "دونات", "Donuts", "دونات", null, 78_000, 0, "donuts"), + new("item_demo_churros", "cat_demo_dessert", "چوروس", "Churros", "تشورو", "شکلات", 92_000, 0, "churros"), + new("item_demo_baklava", "cat_demo_dessert", "باقلوا", "Baklava", "بقلاوة", null, 98_000, 0, "baklava"), + new("item_demo_creme_brulee", "cat_demo_dessert", "کرم بروله", "Crème brûlée", "كريم بروليه", null, 118_000, 0, "creme_brulee"), + ]; + + /// Resolved image URL for catalog seed (manifest → Food-101 fallback → category default). + public static string ResolveItemImageUrl(ItemSeed item) => + Img(item.Id, item.Food101Class); +} + +/// Maps item/category to drink vs food default images. +file static class DemoMenuCategoryKinds +{ + private static readonly HashSet DrinkCategoryIds = new(StringComparer.Ordinal) + { + "cat_demo_drinks", + "cat_demo_cold" + }; + + private static readonly HashSet DrinkFood101Classes = new(StringComparer.OrdinalIgnoreCase) + { + "espresso", "latte", "cappuccino", "mocha", "iced_coffee", "lemonade", "smoothie", "miso_soup" + }; + + public static MenuItemVisualKind KindFor(string itemId, string food101Class) + { + if (itemId.Contains("demo_iced", StringComparison.Ordinal) + || itemId.Contains("demo_cold", StringComparison.Ordinal) + || itemId.Contains("demo_lemonade", StringComparison.Ordinal) + || itemId.Contains("demo_smoothie", StringComparison.Ordinal) + || itemId.Contains("demo_espresso", StringComparison.Ordinal) + || itemId.Contains("demo_latte", StringComparison.Ordinal) + || itemId.Contains("demo_cappuccino", StringComparison.Ordinal) + || itemId.Contains("demo_mocha", StringComparison.Ordinal) + || itemId.Contains("demo_americano", StringComparison.Ordinal) + || itemId.Contains("demo_tea", StringComparison.Ordinal)) + return MenuItemVisualKind.Drink; + + if (DrinkFood101Classes.Contains(food101Class)) + return MenuItemVisualKind.Drink; + + return MenuItemVisualKind.Food; + } +} diff --git a/src/Meezi.Infrastructure/Data/DemoMenuSeeder.cs b/src/Meezi.Infrastructure/Data/DemoMenuSeeder.cs new file mode 100644 index 0000000..9e2aaf7 --- /dev/null +++ b/src/Meezi.Infrastructure/Data/DemoMenuSeeder.cs @@ -0,0 +1,202 @@ +using Meezi.Core.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace Meezi.Infrastructure.Data; + +public static class DemoMenuSeeder +{ + public static async Task EnsureMenuAsync(AppDbContext db, string cafeId, string taxId, ILogger logger) + { + if (!await db.Taxes.AnyAsync(t => t.Id == taxId && t.CafeId == cafeId)) + { + db.Taxes.Add(new Tax + { + Id = taxId, + CafeId = cafeId, + Name = "مالیات", + Rate = 9, + IsDefault = true, + IsRequired = true, + IsCompound = false + }); + } + + var existingCategoryIds = await db.MenuCategories + .Where(c => c.CafeId == cafeId) + .ToDictionaryAsync(c => c.Id, StringComparer.Ordinal); + + var categoriesAdded = 0; + foreach (var cat in DemoMenuCatalog.Categories) + { + if (existingCategoryIds.TryGetValue(cat.Id, out var row)) + { + if (string.IsNullOrWhiteSpace(row.Icon) && !string.IsNullOrWhiteSpace(cat.Icon)) + row.Icon = cat.Icon; + if (string.IsNullOrWhiteSpace(row.IconPresetId) && !string.IsNullOrWhiteSpace(cat.IconPresetId)) + row.IconPresetId = cat.IconPresetId; + if (string.IsNullOrWhiteSpace(row.IconStyle) && !string.IsNullOrWhiteSpace(cat.IconStyle)) + row.IconStyle = cat.IconStyle; + if (string.IsNullOrWhiteSpace(row.NameEn) && !string.IsNullOrWhiteSpace(cat.NameEn)) + row.NameEn = cat.NameEn; + if (string.IsNullOrWhiteSpace(row.NameAr) && cat.NameAr is not null) + row.NameAr = cat.NameAr; + continue; + } + + db.MenuCategories.Add(new MenuCategory + { + Id = cat.Id, + CafeId = cafeId, + 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 + }); + categoriesAdded++; + } + + var existingItemIds = await db.MenuItems + .Where(i => i.CafeId == cafeId) + .Select(i => i.Id) + .ToListAsync(); + + var itemsAdded = 0; + foreach (var item in DemoMenuCatalog.Items) + { + if (existingItemIds.Contains(item.Id)) + continue; + + db.MenuItems.Add(new MenuItem + { + Id = item.Id, + CafeId = cafeId, + CategoryId = item.CategoryId, + 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 (categoriesAdded > 0 || itemsAdded > 0) + await db.SaveChangesAsync(); + + if (categoriesAdded > 0 || itemsAdded > 0) + { + logger.LogInformation( + "Demo menu seed: +{Categories} categories, +{Items} items (catalog total {Total}) for cafe {CafeId}", + categoriesAdded, + itemsAdded, + DemoMenuCatalog.Items.Count, + cafeId); + } + + await EnsureMenuImagesAsync(db, cafeId, logger); + await EnsureMenuTranslationsAsync(db, cafeId, logger); + } + + /// Upserts ImageUrl from catalog/manifest/Food-101 fallbacks. + public static async Task EnsureMenuImagesAsync(AppDbContext db, string cafeId, ILogger logger) + { + var catalogById = DemoMenuCatalog.Items.ToDictionary(i => i.Id, StringComparer.Ordinal); + var items = await db.MenuItems + .Include(i => i.Category) + .Where(i => i.CafeId == cafeId) + .ToListAsync(); + + var updated = 0; + foreach (var row in items) + { + var resolved = catalogById.TryGetValue(row.Id, out var seed) + ? DemoMenuCatalog.ResolveItemImageUrl(seed) + : MenuItemImageDefaults.ResolveImageUrl(row.Id, row.CategoryId, row.Category?.Name); + + if (string.IsNullOrWhiteSpace(resolved)) continue; + + var inCatalog = catalogById.ContainsKey(row.Id); + var shouldUpdate = MenuItemImageDefaults.NeedsImageRepair(row.ImageUrl) || inCatalog; + if (!shouldUpdate || string.Equals(row.ImageUrl, resolved, StringComparison.Ordinal)) + continue; + + row.ImageUrl = resolved; + updated++; + } + + if (updated > 0) + { + await db.SaveChangesAsync(); + logger.LogInformation("Menu image upsert: {Count} items updated for cafe {CafeId}", updated, cafeId); + } + } + + /// Upserts NameEn/NameAr from catalog for demo menu rows. + public static async Task EnsureMenuTranslationsAsync(AppDbContext db, string cafeId, ILogger logger) + { + var catalogItems = DemoMenuCatalog.Items.ToDictionary(i => i.Id, StringComparer.Ordinal); + var catalogCats = DemoMenuCatalog.Categories.ToDictionary(c => c.Id, StringComparer.Ordinal); + + var items = await db.MenuItems.Where(i => i.CafeId == cafeId && catalogItems.Keys.Contains(i.Id)).ToListAsync(); + var itemUpdated = 0; + foreach (var row in items) + { + if (!catalogItems.TryGetValue(row.Id, out var seed)) continue; + var changed = false; + if (string.IsNullOrWhiteSpace(row.NameEn) && !string.IsNullOrWhiteSpace(seed.NameEn)) + { + row.NameEn = seed.NameEn; + changed = true; + } + if (string.IsNullOrWhiteSpace(row.NameAr) && seed.NameAr is not null) + { + row.NameAr = seed.NameAr; + changed = true; + } + if (changed) itemUpdated++; + } + + var categories = await db.MenuCategories.Where(c => c.CafeId == cafeId && catalogCats.Keys.Contains(c.Id)).ToListAsync(); + var catUpdated = 0; + foreach (var row in categories) + { + if (!catalogCats.TryGetValue(row.Id, out var seed)) continue; + var changed = false; + if (string.IsNullOrWhiteSpace(row.NameEn) && !string.IsNullOrWhiteSpace(seed.NameEn)) + { + row.NameEn = seed.NameEn; + changed = true; + } + if (string.IsNullOrWhiteSpace(row.NameAr) && seed.NameAr is not null) + { + row.NameAr = seed.NameAr; + changed = true; + } + if (string.IsNullOrWhiteSpace(row.Icon) && !string.IsNullOrWhiteSpace(seed.Icon)) + { + row.Icon = seed.Icon; + changed = true; + } + if (changed) catUpdated++; + } + + if (itemUpdated > 0 || catUpdated > 0) + { + await db.SaveChangesAsync(); + logger.LogInformation( + "Menu translation upsert: {Items} items, {Cats} categories for cafe {CafeId}", + itemUpdated, + catUpdated, + cafeId); + } + } +} diff --git a/src/Meezi.Infrastructure/Data/DevelopmentDataSeeder.cs b/src/Meezi.Infrastructure/Data/DevelopmentDataSeeder.cs new file mode 100644 index 0000000..c47a7a1 --- /dev/null +++ b/src/Meezi.Infrastructure/Data/DevelopmentDataSeeder.cs @@ -0,0 +1,401 @@ +using Meezi.Core.Branding; +using Meezi.Core.Discover; +using Meezi.Core.Entities; +using Meezi.Core.Enums; +using Meezi.Infrastructure.Branding; +using Meezi.Infrastructure.Discover; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Meezi.Infrastructure.Data; + +public static class DevelopmentDataSeeder +{ + public static async Task SeedAsync(IServiceProvider services) + { + var env = services.GetRequiredService(); + if (!env.IsDevelopment()) + return; + + var logger = services.GetRequiredService().CreateLogger("DevelopmentDataSeeder"); + await using var scope = services.CreateAsyncScope(); + var db = scope.ServiceProvider.GetRequiredService(); + + var cafe = await db.Cafes.FirstOrDefaultAsync(c => c.Slug == "demo-cafe"); + if (cafe is null) + { + cafe = new Cafe + { + Id = "cafe_demo_001", + Name = "کافه دمو", + NameEn = "Demo Cafe", + Slug = "demo-cafe", + City = "تهران", + Address = "تهران، خیابان ولیعصر", + Description = "کافه دمو میزی — مناسب کار، میهمانی و قهوه تخصصی.", + PlanTier = PlanTier.Pro, + PreferredLanguage = "fa", + IsVerified = true, + SnappfoodVendorId = "demo_vendor" + }; + + var owner = new Employee + { + Id = "emp_demo_owner", + CafeId = cafe.Id, + BranchId = "branch_demo_main", + Name = "مدیر دمو", + Phone = "09121234567", + Role = EmployeeRole.Owner, + BaseSalary = 0 + }; + + db.Cafes.Add(cafe); + db.Employees.Add(owner); + await db.SaveChangesAsync(); + + logger.LogInformation("Development seed: cafe slug={Slug}, owner phone={Phone}", cafe.Slug, owner.Phone); + } + else if (string.IsNullOrEmpty(cafe.SnappfoodVendorId)) + { + cafe.SnappfoodVendorId = "demo_vendor"; + await db.SaveChangesAsync(); + } + + if (string.IsNullOrEmpty(cafe.ThemeJson)) + { + cafe.ThemeJson = CafeThemeSerializer.Serialize(new CafeTheme + { + PaletteId = CafeThemeDefaults.PaletteMeeziGreen, + PanelStyle = CafeThemeDefaults.PanelModern, + MenuStyle = CafeThemeDefaults.MenuCards, + Density = CafeThemeDefaults.DensityComfortable, + Radius = CafeThemeDefaults.RadiusMd + }); + await db.SaveChangesAsync(); + } + + if (string.IsNullOrEmpty(cafe.DiscoverProfileJson)) + { + cafe.DiscoverProfileJson = CafeDiscoverProfileSerializer.Serialize(new CafeDiscoverProfile + { + Themes = ["modern", "plants_heavy"], + Size = "cozy", + Vibes = ["quiet", "cozy"], + Occasions = ["date", "friends", "study_work"], + SpaceFeatures = ["indoor", "wifi", "plants"], + NoiseLevel = "quiet", + PriceTier = "mid" + }); + await db.SaveChangesAsync(); + } + + await SeedDemoReviewsAsync(db, cafe.Id, logger); + await SeedDemoBranchAsync(db, cafe.Id, logger); + await SeedDemoOpenShiftsAsync(db, cafe.Id, logger); + + var ownerEmp = await db.Employees.FirstOrDefaultAsync(e => e.Id == "emp_demo_owner"); + if (ownerEmp is not null && ownerEmp.BranchId is null) + { + ownerEmp.BranchId = "branch_demo_main"; + await db.SaveChangesAsync(); + } + + await DemoEmployeesSeeder.EnsureEmployeesAsync(db, cafe.Id, logger); + + const string taxId = "tax_demo_vat"; + await DemoMenuSeeder.EnsureMenuAsync(db, cafe.Id, taxId, logger); + await SeedDemoInventoryAsync(db, cafe.Id, logger); + await DemoCouponSeeder.EnsureCouponsAsync(db, cafe.Id, logger); + await SeedDemoTablesAsync(db, cafe.Id, logger); + + if (!await db.EmployeeSchedules.AnyAsync(s => s.EmployeeId == "emp_demo_owner")) + { + for (var day = 0; day <= 6; day++) + { + db.EmployeeSchedules.Add(new EmployeeSchedule + { + EmployeeId = "emp_demo_owner", + DayOfWeek = day, + ShiftType = day is 5 ? ShiftType.DayOff : ShiftType.Morning + }); + } + + var monthYear = DateTime.UtcNow.ToString("yyyy-MM"); + db.EmployeeSalaries.Add(new EmployeeSalary + { + EmployeeId = "emp_demo_owner", + MonthYear = monthYear, + BaseSalary = 25_000_000, + OvertimePay = 0, + Deductions = 0, + NetSalary = 25_000_000, + IsPaid = false + }); + + await db.SaveChangesAsync(); + logger.LogInformation("Development HR seed for cafe {CafeId}", cafe.Id); + } + + await SeedDemoOrdersAsync(db, cafe.Id, logger); + await DiscoverShowcaseSeeder.SeedAsync(db, logger); + } + + private static async Task SeedDemoBranchAsync(AppDbContext db, string cafeId, ILogger logger) + { + const string branchId = "branch_demo_main"; + if (!await db.Branches.AnyAsync(b => b.Id == branchId)) + { + var now = DateTime.UtcNow; + db.Branches.Add(new Branch + { + Id = branchId, + CafeId = cafeId, + Name = "شعبه اصلی", + City = "تهران", + Address = "تهران، خیابان ولیعصر", + Phone = "02112345678", + IsActive = true, + CreatedAt = now, + UpdatedAt = now + }); + await db.SaveChangesAsync(); + logger.LogInformation("Development branch seed: {BranchId}", branchId); + } + } + + private static async Task SeedDemoOpenShiftsAsync(AppDbContext db, string cafeId, ILogger logger) + { + const string ownerId = "emp_demo_owner"; + var branchIds = await db.Branches + .Where(b => b.CafeId == cafeId && b.IsActive) + .Select(b => b.Id) + .ToListAsync(); + + var added = false; + foreach (var branchId in branchIds) + { + var hasOpen = await db.RegisterShifts.AnyAsync( + s => s.CafeId == cafeId && s.BranchId == branchId && s.Status == ShiftStatus.Open); + if (hasOpen) + continue; + + db.RegisterShifts.Add(new Shift + { + Id = $"shift_open_{branchId}", + CafeId = cafeId, + BranchId = branchId, + OpenedByUserId = ownerId, + OpenedAt = DateTime.UtcNow, + OpeningCash = 0, + ExpectedCash = 0, + Status = ShiftStatus.Open + }); + added = true; + } + + if (added) + { + await db.SaveChangesAsync(); + logger.LogInformation("Development open-shift seed for cafe {CafeId}", cafeId); + } + } + + private static async Task SeedDemoInventoryAsync(AppDbContext db, string cafeId, ILogger logger) + { + if (await db.Ingredients.AnyAsync(i => i.CafeId == cafeId)) + return; + + db.Ingredients.AddRange( + new Ingredient + { + Id = "ing_demo_milk", + CafeId = cafeId, + Name = "شیر", + Unit = "میلی‌لیتر", + QuantityOnHand = 12000, + ReorderLevel = 2000, + ParLevel = 12000, + UnitCost = 80, + LowStockWarningPercent = 20 + }, + new Ingredient + { + Id = "ing_demo_coffee", + CafeId = cafeId, + Name = "پودر قهوه", + Unit = "گرم", + QuantityOnHand = 500, + ReorderLevel = 100, + ParLevel = 500, + UnitCost = 12, + LowStockWarningPercent = 20 + }, + new Ingredient + { + Id = "ing_demo_cups", + CafeId = cafeId, + Name = "لیوان یکبارمصرف", + Unit = "عدد", + QuantityOnHand = 80, + ReorderLevel = 20, + ParLevel = 100, + UnitCost = 500, + LowStockWarningPercent = 20 + }); + + await db.SaveChangesAsync(); + + var espresso = await db.MenuItems + .FirstOrDefaultAsync(m => m.CafeId == cafeId && m.Name.Contains("اسپرسو")); + if (espresso is not null) + { + db.MenuItemIngredients.AddRange( + new MenuItemIngredient + { + Id = "mii_demo_espresso_coffee", + CafeId = cafeId, + MenuItemId = espresso.Id, + IngredientId = "ing_demo_coffee", + QuantityPerUnit = 10 + }, + new MenuItemIngredient + { + Id = "mii_demo_espresso_cup", + CafeId = cafeId, + MenuItemId = espresso.Id, + IngredientId = "ing_demo_cups", + QuantityPerUnit = 1 + }); + await db.SaveChangesAsync(); + } + logger.LogInformation("Development inventory seed for cafe {CafeId}", cafeId); + } + + private static async Task SeedDemoTablesAsync(AppDbContext db, string cafeId, ILogger logger) + { + var specs = new (string Id, string Number, int Capacity, string? Floor, string? QrCode)[] + { + ("table_demo_1", "1", 4, "همکف", "demo_table_01"), + ("table_demo_2", "2", 2, "همکف", null), + ("table_demo_3", "3", 4, "همکف", null), + ("table_demo_4", "4", 6, "بالکن", null), + ("table_demo_5", "5", 4, "بالکن", null), + ("table_demo_6", "6", 2, "بالکن", null), + ("table_demo_7", "7", 8, "سالن VIP", null), + ("table_demo_8", "8", 4, "سالن VIP", null), + }; + + foreach (var s in specs) + { + if (await db.Tables.AnyAsync(t => t.Id == s.Id)) + continue; + + db.Tables.Add(new Table + { + Id = s.Id, + CafeId = cafeId, + BranchId = "branch_demo_main", + Number = s.Number, + Capacity = s.Capacity, + Floor = s.Floor, + QrCode = s.QrCode ?? Guid.NewGuid().ToString("N"), + IsActive = true + }); + } + + await db.SaveChangesAsync(); + logger.LogInformation("Development tables seed (8 tables, QR: demo_table_01) for cafe {CafeId}", cafeId); + } + + private static async Task SeedDemoReviewsAsync(AppDbContext db, string cafeId, ILogger logger) + { + if (await db.CafeReviews.AnyAsync(r => r.CafeId == cafeId)) + return; + + db.CafeReviews.AddRange( + new CafeReview + { + CafeId = cafeId, + AuthorName = "سارا", + Rating = 5, + Comment = "قهوه و فضا عالی بود.", + CreatedAt = DateTime.UtcNow.AddDays(-3) + }, + new CafeReview + { + CafeId = cafeId, + AuthorName = "علی", + Rating = 4, + Comment = "سرویس سریع، کیک خوشمزه.", + CreatedAt = DateTime.UtcNow.AddDays(-1) + }); + + await db.SaveChangesAsync(); + logger.LogInformation("Development reviews seed for cafe {CafeId}", cafeId); + } + + private static async Task SeedDemoOrdersAsync(AppDbContext db, string cafeId, ILogger logger) + { + if (await db.Orders.AnyAsync(o => o.CafeId == cafeId)) + return; + + var latte = await db.MenuItems.FirstOrDefaultAsync(m => m.Id == "item_demo_latte"); + var cake = await db.MenuItems.FirstOrDefaultAsync(m => m.Id == "item_demo_cake"); + if (latte is null || cake is null) + return; + + var customer = new Customer + { + Id = "cust_demo_reports", + CafeId = cafeId, + Name = "مشتری گزارش", + Phone = "09120000001", + Group = CustomerGroup.Regular, + CreatedAt = DateTime.UtcNow.AddDays(-10) + }; + db.Customers.Add(customer); + + for (var i = 0; i < 7; i++) + { + var createdAt = DateTime.UtcNow.AddDays(-i).AddHours(-2); + var subtotal = 215_000m; + var tax = Math.Round(subtotal * 0.09m, 0); + var order = new Order + { + Id = $"order_demo_{i}", + CafeId = cafeId, + CustomerId = i % 2 == 0 ? customer.Id : null, + OrderType = OrderType.DineIn, + Status = OrderStatus.Delivered, + DisplayNumber = i + 1, + Subtotal = subtotal, + TaxTotal = tax, + DiscountAmount = i == 0 ? 15_000m : 0, + Total = subtotal + tax - (i == 0 ? 15_000m : 0), + CreatedAt = createdAt + }; + db.Orders.Add(order); + db.OrderItems.AddRange( + new OrderItem + { + OrderId = order.Id, + MenuItemId = latte.Id, + Quantity = 1, + UnitPrice = latte.Price + }, + new OrderItem + { + OrderId = order.Id, + MenuItemId = cake.Id, + Quantity = 1, + UnitPrice = cake.Price + }); + } + + await db.SaveChangesAsync(); + logger.LogInformation("Development reports seed: 7 demo orders for cafe {CafeId}", cafeId); + } +} diff --git a/src/Meezi.Infrastructure/Data/DiscoverShowcaseCatalog.cs b/src/Meezi.Infrastructure/Data/DiscoverShowcaseCatalog.cs new file mode 100644 index 0000000..7afee97 --- /dev/null +++ b/src/Meezi.Infrastructure/Data/DiscoverShowcaseCatalog.cs @@ -0,0 +1,146 @@ +using Meezi.Core.Discover; +using Meezi.Core.Enums; + +namespace Meezi.Infrastructure.Data; + +public static class DiscoverShowcaseCatalog +{ + public sealed record ShowcaseCafe( + string Id, + string Slug, + string Name, + string City, + string Address, + string Description, + CafeDiscoverProfile Profile, + PlanTier PlanTier, + int MenuTemplateIndex, + string OwnerPhone, + IReadOnlyList? Badges = null); + + public static IReadOnlyList Cafes { get; } = + [ + Cafe("cafe_sc_01", "cafe-mokhteh-tehran", "کافه مخته", "تهران", "تهران، ولیعصر، نرسیده به پارک ساعی", + "رستری تخصصی قهوه و دسر؛ فضای آرام برای کار و قرار دو نفره. اسپرسو، لاته، چیزکیک.", + Profile(["roastery", "modern"], "cozy", ["quiet", "cozy"], ["study_work", "date"], ["indoor", "wifi"], "quiet", "mid"), 0, "09131000001"), + Cafe("cafe_sc_02", "cafe-sibo-karaj", "کافه سیب", "کرج", "کرج، گوهردشت، بلوار موذن‌نیا", + "کافه دنج کرج با قهوه دمی و بیکری؛ مناسب خانواده و صبحانه آخر هفته.", + Profile(["brunch", "plants_heavy"], "medium", ["casual", "cozy"], ["family", "friends"], ["indoor", "kids_friendly"], "moderate", "budget"), 1, "09131000002"), + Cafe("cafe_sc_03", "cafe-ava-valiasr", "کافه آوا", "تهران", "تهران، ولیعصر، بالاتر از میدان ونک", + "کافه مدرن با نور طبیعی؛ لاته آرت و نوشیدنی‌های فصل. جستجو: قهوه تخصصی ولیعصر.", + Profile(["modern", "instagrammable"], "medium", ["trendy", "lively"], ["friends", "quick_coffee"], ["indoor", "terrace"], "moderate", "mid"), 0, "09131000003"), + Cafe("cafe_sc_04", "restaurant-sang-tehran", "رستوران سنگ", "تهران", "تهران، جردن، خیابان ناهید شرقی", + "غذای ایرانی و بین‌المللی؛ فضای لوکس برای شام و جشن. استیک، پاستا، سالاد.", + Profile(["luxury", "heritage"], "large", ["luxury", "romantic"], ["celebration", "business_meeting"], ["indoor", "private_room"], "quiet", "premium"), 2, "09131000004", PlanTier.Business), + Cafe("cafe_sc_05", "cafe-ketab-niavaran", "کافه کتاب نیاوران", "تهران", "تهران، نیاوران، خیابان باهنر", + "بوفه کتاب با قهوه و چای؛ فضای ساکت برای درس و کار. دمنوش، کیک، ساندویچ.", + Profile(["book_cafe", "quiet"], "cozy", ["quiet", "study_friendly"], ["study_work", "solo"], ["indoor", "wifi"], "quiet", "mid"), 0, "09131000005"), + Cafe("cafe_sc_06", "cafe-rooz-karaj", "کافه روز", "کرج", "کرج، عظیمیه، میدان جهاد", + "کافه روز کرج؛ اسموتی، قهوه سرد، صبحانه کامل. مناسب جوانان و دورهمی.", + Profile(["modern", "brunch"], "medium", ["lively", "casual"], ["friends", "breakfast"], ["outdoor", "wifi"], "moderate", "budget"), 1, "09131000006"), + Cafe("cafe_sc_07", "cafe-gol-reza", "کافه گل رضا", "تهران", "تهران، سعادت‌آباد، میدان کاج", + "کافه گل‌دار و دکور سنتی مدرن؛ عالی برای عکاسی و قرار عاشقانه.", + Profile(["persian_traditional", "instagrammable"], "cozy", ["romantic", "cozy"], ["date"], ["indoor", "plants"], "quiet", "mid"), 4, "09131000007"), + Cafe("cafe_sc_08", "cafe-shab-enghelab", "کافه شب", "تهران", "تهران، انقلاب، خیابان ۱۲ فروردین", + "کافه دیروقت؛ قهوه و دسر تا نیمه‌شب. موسیقی ملایم، فضای شب‌گاه.", + Profile(["late_night", "industrial"], "medium", ["lively", "trendy"], ["friends", "finding_someone"], ["indoor", "live_music"], "lively", "mid"), 0, "09131000008"), + Cafe("cafe_sc_09", "cafe-darya-karaj", "کافه دریا", "کرج", "کرج، مارلیک، بلوار ارم", + "تراس باز با نوشیدنی سرد؛ مناسب تابستان و دورهمی دوستانه در کرج.", + Profile(["modern"], "spacious", ["casual", "lively"], ["friends", "family"], ["outdoor", "terrace"], "moderate", "mid"), 0, "09131000009"), + Cafe("cafe_sc_10", "cafe-nan-o-nam", "نان و نمک", "تهران", "تهران، پاسداران، خیابان گلستان هفتم", + "بیکری-کافه با نان تازه و صبحانه؛ کره محلی، مربا، قهوه فیلتر.", + Profile(["brunch", "heritage"], "cozy", ["cozy", "traditional"], ["breakfast", "family"], ["indoor"], "moderate", "budget"), 1, "09131000010"), + Cafe("cafe_sc_11", "cafe-terrace-farmanieh", "تراس فرمانیه", "تهران", "تهران، فرمانیه، خیابان کلاهدوز", + "کافه تراس با ویوی شهر؛ قهوه ویژه و دسر فصل. مناسب قرار و عکاسی.", + Profile(["luxury", "instagrammable"], "large", ["romantic", "luxury"], ["date", "celebration"], ["terrace", "outdoor"], "quiet", "premium"), 3, "09131000011", PlanTier.Enterprise, ["award_winner", "roastery"]), + Cafe("cafe_sc_12", "cafe-minimal-karaj", "مینیمال کرج", "کرج", "کرج، گلشهر، بلوار امیرکبیر", + "کافه مینیمال کرج؛ اسپرسو دقیق و فضای سفید آرام.", + Profile(["minimal", "scandi"], "tiny", ["quiet", "study_friendly"], ["solo", "study_work"], ["indoor", "wifi"], "quiet", "mid"), 0, "09131000012"), + Cafe("cafe_sc_13", "cafe-chocolate-tehran", "خانه شکلات", "تهران", "تهران، زعفرانیه، خیابان مقدس اردبیلی", + "دسرخانه شکلات و قهوه؛ تارت، شکلات دست‌ساز، هات چاکلت.", + Profile(["dessert_focus", "luxury"], "cozy", ["romantic", "cozy"], ["date"], ["indoor"], "quiet", "premium"), 3, "09131000013"), + Cafe("cafe_sc_14", "cafe-vintage-tajrish", "کافه وینتیج تجریش", "تهران", "تهران، تجریش، میدان قدس", + "دکور قدیمی تهران؛ قهوه ترک و کیک خانگی. فضای نوستالژیک.", + Profile(["vintage", "heritage"], "cozy", ["traditional", "cozy"], ["friends", "solo"], ["indoor"], "moderate", "mid"), 0, "09131000014"), + Cafe("cafe_sc_15", "cafe-work-hub-karaj", "هاب کار کرج", "کرج", "کرج، جهانشهر، بلوار شهید مطهری", + "فضای کار اشتراکی با کافه؛ وای‌فای قوی، قهوه نامحدود، ساندویچ.", + Profile(["modern", "book_cafe"], "medium", ["study_friendly", "quiet"], ["study_work", "business_meeting"], ["indoor", "wifi"], "quiet", "budget"), 0, "09131000015"), + Cafe("cafe_sc_16", "cafe-pet-pardis", "کافه پت پاردیس", "تهران", "تهران، پردیس، فاز ۱", + "کافه پت‌فرندلی؛ نوشیدنی گیاهی و فضای باز برای صاحبان سگ.", + Profile(["plants_heavy", "modern"], "spacious", ["casual", "lively"], ["friends", "family"], ["outdoor", "pet_friendly"], "moderate", "mid"), 0, "09131000016", badges: ["pet_friendly"]), + Cafe("cafe_sc_17", "restaurant-sabz-karaj", "رستوران سبز", "کرج", "کرج، مشکین‌دشت، بلوار آزادی", + "رستوران گیاهی و سالم کرج؛ بول، سالاد، آبمیوه تازه.", + Profile(["modern", "plants_heavy"], "medium", ["casual"], ["family", "friends"], ["indoor", "outdoor"], "moderate", "mid"), 2, "09131000017"), + Cafe("cafe_sc_18", "cafe-roastery-darabad", "رستری دارآباد", "تهران", "تهران، دارآباد، خیابان اصلی", + "رستری قهوه تخصصی با دم‌آوری دستی؛ تست پروفایل‌های مختلف.", + Profile(["roastery", "industrial"], "medium", ["trendy"], ["quick_coffee", "friends"], ["indoor"], "moderate", "mid"), 0, "09131000018"), + Cafe("cafe_sc_19", "cafe-family-mehri", "کافه مهری", "تهران", "تهران، تهرانپارس، فلکه اول", + "کافه خانوادگی با فضای بازی کودک؛ صبحانه و ناهار سبک.", + Profile(["brunch"], "large", ["casual", "cozy"], ["family"], ["indoor", "kids_friendly"], "moderate", "budget"), 1, "09131000019"), + Cafe("cafe_sc_20", "cafe-laleh-karaj", "کافه لاله", "کرج", "کرج، باغستان، خیابان شهید بهشتی", + "کافه گل لاله کرج؛ دمنوش گل و قهوه ترک. فضای زنانه دوستانه.", + Profile(["plants_heavy", "persian_traditional"], "cozy", ["cozy", "quiet"], ["friends", "solo"], ["indoor", "plants"], "quiet", "budget"), 4, "09131000020"), + Cafe("cafe_sc_21", "cafe-business-iran", "کافه ایران‌زمین", "تهران", "تهران، آفریقا، برج میلاد نزدیک", + "مناسب جلسه کاری؛ قهوه سریع، صبحانه executive، سالاد.", + Profile(["modern", "luxury"], "medium", ["luxury", "quiet"], ["business_meeting"], ["indoor", "wifi", "private_room"], "quiet", "premium"), 2, "09131000021"), + Cafe("cafe_sc_22", "cafe-sunset-karaj", "غروب کرج", "کرج", "کرج، حصارک، جاده چالوس", + "ویوی کوه و غروب؛ قهوه دمی و کیک هویج. مناسب آخر هفته.", + Profile(["instagrammable"], "cozy", ["romantic", "casual"], ["date", "friends"], ["outdoor", "terrace"], "quiet", "mid"), 0, "09131000022"), + Cafe("cafe_sc_23", "cafe-gaming-tehran", "کافه گیم", "تهران", "تهران، یوسف‌آباد، خیابان جهان‌آرا", + "کافه گیمینگ؛ نوشیدنی انرژی‌زا و اسنک؛ فضای پرانرژی جوانان.", + Profile(["modern", "late_night"], "medium", ["lively", "trendy"], ["friends"], ["indoor"], "lively", "budget"), 0, "09131000023"), + Cafe("cafe_sc_24", "cafe-honey-karaj", "عسل کرج", "کرج", "کرج، طالقانی، میدان شهدا", + "کافه با عسل محلی و چای؛ کیک عسل، دمنوش گیاهی کرج.", + Profile(["heritage", "persian_traditional"], "cozy", ["traditional", "cozy"], ["family"], ["indoor"], "quiet", "budget"), 4, "09131000024"), + Cafe("cafe_sc_25", "cafe-art-tehran", "کافه هنر", "تهران", "تهران، ایرانشهر، خیابان نواب", + "گالری-کافه؛ قهوه و نمایشگاه موقت. فضای هنری و خلاق.", + Profile(["artistic", "vintage"], "medium", ["artistic", "quiet"], ["solo", "friends"], ["indoor"], "quiet", "mid"), 0, "09131000025"), + Cafe("cafe_sc_26", "cafe-quick-karaj", "ایستگاه قهوه کرج", "کرج", "کرج، کرج مرکزی، میدان آزادگان", + "قهوه سریع کرج؛ مناسب مسیر کار و دانشجو. ساندویچ و اسپرسو.", + Profile(["modern"], "tiny", ["casual"], ["quick_coffee", "study_work"], ["indoor"], "moderate", "budget"), 0, "09131000026"), + Cafe("cafe_sc_27", "restaurant-caspian", "رستوران کاسپین", "تهران", "تهران، الهیه، خیابان فرشته", + "غذای دریایی و فیش؛ پاستا دریایی، سالاد، mocktail.", + Profile(["luxury", "modern"], "large", ["luxury", "romantic"], ["celebration", "date"], ["indoor"], "quiet", "premium"), 2, "09131000027", PlanTier.Enterprise, ["verified_partner"]), + Cafe("cafe_sc_28", "cafe-mountain-karaj", "کافه کوهستان", "کرج", "کرج، محمدشهر، انتهای بلوار ارم", + "فضای کوهستانی کرج؛ چای کوهی و سوخاری. هوای خنک تراس.", + Profile(["modern"], "spacious", ["casual", "cozy"], ["family", "friends"], ["outdoor", "terrace"], "moderate", "mid"), 2, "09131000028"), + Cafe("cafe_sc_29", "cafe-vegan-tehran", "کافه وگان", "تهران", "تهران، جمالزاده، خیابان مخبر", + "منوی وگان و گیاهی؛ لاته جو، شیر بادام، دسر وگان.", + Profile(["modern", "plants_heavy"], "cozy", ["trendy", "casual"], ["friends", "solo"], ["indoor", "wifi"], "quiet", "mid"), 0, "09131000029", badges: ["eco_friendly"]), + Cafe("cafe_sc_30", "cafe-royal-karaj", "کافه رویال کرج", "کرج", "کرج، عظیمیه، برج بلور", + "کافه لوکس کرج؛ دسر فرانسوی و قهوه اسپشیالتی. مناسب جشن.", + Profile(["luxury", "dessert_focus"], "large", ["luxury", "romantic"], ["celebration", "date"], ["indoor", "private_room"], "quiet", "premium"), 3, "09131000030", PlanTier.Enterprise, ["award_winner", "verified_partner"]), + ]; + + private static ShowcaseCafe Cafe( + string id, + string slug, + string name, + string city, + string address, + string description, + CafeDiscoverProfile profile, + int menuTemplate, + string phone, + PlanTier plan = PlanTier.Pro, + IReadOnlyList? badges = null) => + new(id, slug, name, city, address, description, profile, plan, menuTemplate, phone, badges); + + private static CafeDiscoverProfile Profile( + string[] themes, + string size, + string[] vibes, + string[] occasions, + string[] space, + string noise, + string price) => new() + { + Themes = themes.ToList(), + Size = size, + Vibes = vibes.ToList(), + Occasions = occasions.ToList(), + SpaceFeatures = space.ToList(), + NoiseLevel = noise, + PriceTier = price, + }; +} diff --git a/src/Meezi.Infrastructure/Data/DiscoverShowcaseMenus.cs b/src/Meezi.Infrastructure/Data/DiscoverShowcaseMenus.cs new file mode 100644 index 0000000..ceace25 --- /dev/null +++ b/src/Meezi.Infrastructure/Data/DiscoverShowcaseMenus.cs @@ -0,0 +1,78 @@ +namespace Meezi.Infrastructure.Data; + +/// Rotating menu templates for discover showcase cafés. +public static class DiscoverShowcaseMenus +{ + public sealed record MenuTemplate( + IReadOnlyList Categories, + IReadOnlyList Items); + + public static IReadOnlyList Templates { get; } = + [ + CoffeeShopTemplate(), + BrunchTemplate(), + RestaurantTemplate(), + DessertTemplate(), + TraditionalTemplate(), + ]; + + private static MenuTemplate CoffeeShopTemplate() => new( + [ + new("cat_sc_hot", "قهوه تخصصی", "Specialty coffee", null, 1, IconPresetId: "drinks-hot", IconStyle: "gradient"), + new("cat_sc_cold", "نوشیدنی سرد", "Cold drinks", null, 2, IconPresetId: "drinks-cold", IconStyle: "modern"), + new("cat_sc_snack", "میان‌وعده", "Snacks", null, 3, IconPresetId: "food-mains", IconStyle: "flat"), + ], + [ + new("item_sc_espresso", "cat_sc_hot", "اسپرسو", "Espresso", null, "دوبل سینگل", 70_000, 0, "espresso"), + new("item_sc_latte", "cat_sc_hot", "لاته", "Latte", null, "شیر بخار", 125_000, 0, "latte"), + new("item_sc_mocha", "cat_sc_hot", "موکا", "Mocha", null, null, 140_000, 0, "mocha"), + new("item_sc_iced", "cat_sc_cold", "آیس آمریکانو", "Iced americano", null, null, 115_000, 0, "iced_coffee"), + new("item_sc_cake", "cat_sc_snack", "چیزکیک", "Cheesecake", null, null, 165_000, 10, "cheesecake"), + ]); + + private static MenuTemplate BrunchTemplate() => new( + [ + new("cat_sc_br", "صبحانه", "Breakfast", null, 1, IconPresetId: "breakfast", IconStyle: "pastel"), + new("cat_sc_br_d", "نوشیدنی", "Drinks", null, 2, IconPresetId: "drinks-hot"), + ], + [ + new("item_sc_omelette", "cat_sc_br", "املت", "Omelette", null, "نان سنگک", 155_000, 0, "omelette"), + new("item_sc_avocado", "cat_sc_br", "توست آووکادو", "Avocado toast", null, null, 195_000, 0, "avocado_toast"), + new("item_sc_tea", "cat_sc_br_d", "چای", "Tea", null, null, 65_000, 0, "miso_soup"), + ]); + + private static MenuTemplate RestaurantTemplate() => new( + [ + new("cat_sc_main", "غذای اصلی", "Mains", null, 1, IconPresetId: "food-mains", IconStyle: "bold"), + new("cat_sc_salad", "سالاد", "Salads", null, 2, IconPresetId: "food-mains"), + new("cat_sc_drink", "نوشیدنی", "Drinks", null, 3, IconPresetId: "drinks-cold"), + ], + [ + new("item_sc_burger", "cat_sc_main", "همبرگر", "Burger", null, null, 265_000, 0, "hamburger"), + new("item_sc_pasta", "cat_sc_main", "پاستا", "Pasta", null, null, 245_000, 0, "spaghetti_bolognese"), + new("item_sc_salad", "cat_sc_salad", "سالاد سزار", "Caesar salad", null, null, 185_000, 0, "caesar_salad"), + new("item_sc_lemon", "cat_sc_drink", "لیموناد", "Lemonade", null, null, 95_000, 0, "lemonade"), + ]); + + private static MenuTemplate DessertTemplate() => new( + [ + new("cat_sc_des", "دسر", "Desserts", null, 1, IconPresetId: "dessert", IconStyle: "soft"), + new("cat_sc_des_d", "قهوه", "Coffee", null, 2, IconPresetId: "drinks-hot"), + ], + [ + new("item_sc_tiramisu", "cat_sc_des", "تیرامیسو", "Tiramisu", null, null, 175_000, 0, "tiramisu"), + new("item_sc_macaron", "cat_sc_des", "ماکارون", "Macaron", null, null, 95_000, 0, "macarons"), + new("item_sc_capp", "cat_sc_des_d", "کاپوچینو", "Cappuccino", null, null, 110_000, 0, "cappuccino"), + ]); + + private static MenuTemplate TraditionalTemplate() => new( + [ + new("cat_sc_tr", "نوشیدنی سنتی", "Traditional drinks", null, 1, IconPresetId: "drinks-hot", IconStyle: "duotone"), + new("cat_sc_tr_f", "غذا", "Food", null, 2, IconPresetId: "food-mains"), + ], + [ + new("item_sc_tea_tr", "cat_sc_tr", "چای ایرانی", "Persian tea", null, null, 55_000, 0, "miso_soup"), + new("item_sc_kebab", "cat_sc_tr_f", "چلوکباب", "Kebab plate", null, null, 320_000, 0, "steak"), + new("item_sc_soup", "cat_sc_tr_f", "سوپ", "Soup", null, null, 120_000, 0, "miso_soup"), + ]); +} diff --git a/src/Meezi.Infrastructure/Data/DiscoverShowcaseSeeder.cs b/src/Meezi.Infrastructure/Data/DiscoverShowcaseSeeder.cs new file mode 100644 index 0000000..2a413e9 --- /dev/null +++ b/src/Meezi.Infrastructure/Data/DiscoverShowcaseSeeder.cs @@ -0,0 +1,239 @@ +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; + +/// Seeds 30 Persian showcase cafés for public discover (development only). +public static class DiscoverShowcaseSeeder +{ + 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); + 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, + 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 (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}"; +} diff --git a/src/Meezi.Infrastructure/Data/Food101ImageFallbacks.cs b/src/Meezi.Infrastructure/Data/Food101ImageFallbacks.cs new file mode 100644 index 0000000..40c6d04 --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Food101ImageFallbacks.cs @@ -0,0 +1,109 @@ +namespace Meezi.Infrastructure.Data; + +/// +/// Unsplash fallbacks per Food-101 class folder name (Kaggle dataset layout). +/// Used when menu-image-manifest.json has no entry or local JPEG was not imported. +/// +public static class Food101ImageFallbacks +{ + private static readonly Dictionary ClassUrls = new(StringComparer.OrdinalIgnoreCase) + { + ["apple_pie"] = "https://images.unsplash.com/photo-1535920527002-b43e668c3097?w=600", + ["avocado_toast"] = "https://images.unsplash.com/photo-1541519221064-49632fb3380e?w=600", + ["baklava"] = "https://images.unsplash.com/photo-1598110756419-84b30681f41f?w=600", + ["beef_carpaccio"] = "https://images.unsplash.com/photo-1600891963295-d66a269b9202?w=600", + ["beef_tartare"] = "https://images.unsplash.com/photo-1600891963295-d66a269b9202?w=600", + ["beet_salad"] = "https://images.unsplash.com/photo-1512621776951-a57141f2eefd?w=600", + ["bruschetta"] = "https://images.unsplash.com/photo-1572695157366-8486c5880882?w=600", + ["brownie"] = "https://images.unsplash.com/photo-1606313564200-e75d5e30476e?w=600", + ["caesar_salad"] = "https://images.unsplash.com/photo-1546793665-c74683f339c1?w=600", + ["caprese_salad"] = "https://images.unsplash.com/photo-1592417817098-8fd3d9eb14a5?w=600", + ["cappuccino"] = "https://images.unsplash.com/photo-1572442388796-11668a67e3d0?w=600", + ["carrot_cake"] = "https://images.unsplash.com/photo-1621303837534-610466b814da?w=600", + ["cheesecake"] = "https://images.unsplash.com/photo-1524351199678-941a58cfcc36?w=600", + ["chocolate_cake"] = "https://images.unsplash.com/photo-1578985545062-69928b1d9587?w=600", + ["churros"] = "https://images.unsplash.com/photo-1627482299165-0883a056a48f?w=600", + ["club_sandwich"] = "https://images.unsplash.com/photo-1528735602780-2552fd46c7af?w=600", + ["creme_brulee"] = "https://images.unsplash.com/photo-1470309864661-683be0ef7eaf?w=600", + ["croque_madame"] = "https://images.unsplash.com/photo-1555507036342-9231d37c10f3?w=600", + ["cup_cakes"] = "https://images.unsplash.com/photo-1614707267537-b85a1e38f271?w=600", + ["donuts"] = "https://images.unsplash.com/photo-1551024506-0bccd28d3071?w=600", + ["dumplings"] = "https://images.unsplash.com/photo-1496116218417-1a781a1c08c2?w=600", + ["edamame"] = "https://images.unsplash.com/photo-1459411621453-7b03977d4a4d?w=600", + ["eggs_benedict"] = "https://images.unsplash.com/photo-1608039819502-3d5a2a2e0b0e?w=600", + ["espresso"] = "https://images.unsplash.com/photo-1509042239860-f550ce710b93?w=600&auto=format&fit=crop", + ["falafel"] = "https://images.unsplash.com/photo-1601050690597-df5748fb5cee?w=600", + ["french_fries"] = "https://images.unsplash.com/photo-1573080496219-76b9e6909700?w=600", + ["french_toast"] = "https://images.unsplash.com/photo-1484723091739-30a329e1f0c4?w=600", + ["fried_calamari"] = "https://images.unsplash.com/photo-1599487488170-d11ec9c172f0?w=600", + ["garlic_bread"] = "https://images.unsplash.com/photo-1619535852122-9f0362cc8b0e?w=600", + ["gnocchi"] = "https://images.unsplash.com/photo-1551183053-bf33a48c970a?w=600", + ["greek_salad"] = "https://images.unsplash.com/photo-1540189549336-e6e99c3679fe?w=600", + ["grilled_cheese_sandwich"] = "https://images.unsplash.com/photo-1528735602780-2552fd46c7af?w=600", + ["grilled_salmon"] = "https://images.unsplash.com/photo-1467003909585-2f8a72700288?w=600", + ["guacamole"] = "https://images.unsplash.com/photo-1529084963126-48f862a7038c?w=600", + ["hamburger"] = "https://images.unsplash.com/photo-1568901346375-23c9450c58cd?w=600", + ["hot_and_sour_soup"] = "https://images.unsplash.com/photo-1547592160-23ac45744acd?w=600", + ["hot_dog"] = "https://images.unsplash.com/photo-1612392062631-94de6c5a533f?w=600", + ["hummus"] = "https://images.unsplash.com/photo-1626208082043-e6319abfeec2?w=600", + ["ice_cream"] = "https://images.unsplash.com/photo-1563805042-7684c019e1cb?w=600", + ["iced_coffee"] = "https://images.unsplash.com/photo-1517487881594-2787aeee8f58?w=600&auto=format&fit=crop", + ["lasagna"] = "https://images.unsplash.com/photo-1574894709920-11b28e7367e3?w=600", + ["latte"] = "https://images.unsplash.com/photo-1461023058943-07fcbe16d735?w=600", + ["lemonade"] = "https://images.unsplash.com/photo-1523672990561-64c16245f769?w=600", + ["lobster_bisque"] = "https://images.unsplash.com/photo-1547592160-23ac45744acd?w=600", + ["macaroni_and_cheese"] = "https://images.unsplash.com/photo-1543339493-18da843d4cb4?w=600", + ["miso_soup"] = "https://images.unsplash.com/photo-1556679343-c7306c1976bc?w=600", + ["mocha"] = "https://images.unsplash.com/photo-1577887233537-a81b387b125e?w=600", + ["mushroom_risotto"] = "https://images.unsplash.com/photo-1476124362071-b9f2c96ef2db?w=600", + ["nachos"] = "https://images.unsplash.com/photo-1513459032971-41fd1e48ff3c?w=600", + ["omelette"] = "https://images.unsplash.com/photo-1525351484343-752d43d363f1?w=600", + ["onion_rings"] = "https://images.unsplash.com/photo-1630431341973-02b95b531aa4?w=600", + ["oysters"] = "https://images.unsplash.com/photo-1514392043407-4f468d2f5c9e?w=600", + ["paella"] = "https://images.unsplash.com/photo-1534084650011-4c4d81e8ca4b?w=600", + ["pancakes"] = "https://images.unsplash.com/photo-1567620905732-2d1ec7ab7440?w=600", + ["panna_cotta"] = "https://images.unsplash.com/photo-1488477181946-6428a029177a?w=600", + ["pasta_carbonara"] = "https://images.unsplash.com/photo-1621996346565-e3dbc646d9a9?w=600", + ["pho"] = "https://images.unsplash.com/photo-1591814468924-caf36d123dd6?w=600", + ["pizza"] = "https://images.unsplash.com/photo-1513104890138-7c749659a591?w=600", + ["pork_chop"] = "https://images.unsplash.com/photo-1432139558640-143f293648c5?w=600", + ["prime_rib"] = "https://images.unsplash.com/photo-1546833999-b9f581a1996d?w=600", + ["ramen"] = "https://images.unsplash.com/photo-1569718212165-3a8278d5f624?w=600", + ["risotto"] = "https://images.unsplash.com/photo-1476124362071-b9f2c96ef2db?w=600", + ["samosa"] = "https://images.unsplash.com/photo-1601050690597-df5748fb5cee?w=600", + ["sashimi"] = "https://images.unsplash.com/photo-1579584425555-c3ce17fd4351?w=600", + ["scallops"] = "https://images.unsplash.com/photo-1599021452684-36e82be5777f?w=600", + ["shawarma"] = "https://images.unsplash.com/photo-1529006557810-274adbcb39d8?w=600", + ["smoothie"] = "https://images.unsplash.com/photo-1505252585463-0433371f7f6b?w=600", + ["spaghetti_bolognese"] = "https://images.unsplash.com/photo-1622973536968-77544a8a4e0e?w=600", + ["spaghetti_carbonara"] = "https://images.unsplash.com/photo-1612874741227-866d1aeeecd1?w=600", + ["spring_rolls"] = "https://images.unsplash.com/photo-1526318896985-4d29c0903299?w=600", + ["steak"] = "https://images.unsplash.com/photo-1600891963295-d66a269b9202?w=600", + ["strawberry_shortcake"] = "https://images.unsplash.com/photo-1464349095436-e59b7d591c4f?w=600", + ["sushi"] = "https://images.unsplash.com/photo-1579584425555-c3ce17fd4351?w=600", + ["tacos"] = "https://images.unsplash.com/photo-1565299585323-38174c4aab1e?w=600", + ["tiramisu"] = "https://images.unsplash.com/photo-1571877227200-a0d98ea607e9?w=600", + ["tuna_tartare"] = "https://images.unsplash.com/photo-1544025162-d766942659778?w=600", + ["waffles"] = "https://images.unsplash.com/photo-1567818735240-7acbb4b7e34c?w=600", + }; + + public static bool TryGetUrl(string food101Class, out string url) + { + if (ClassUrls.TryGetValue(food101Class, out var found) && !string.IsNullOrWhiteSpace(found)) + { + url = found; + return true; + } + + url = ""; + return false; + } + + public static string Resolve(string? food101Class, MenuItemVisualKind kind) + { + if (!string.IsNullOrWhiteSpace(food101Class) && TryGetUrl(food101Class, out var url)) + return url; + + return MenuItemImageDefaults.GetDefaultImageUrl(kind); + } +} diff --git a/src/Meezi.Infrastructure/Data/MenuImageManifest.cs b/src/Meezi.Infrastructure/Data/MenuImageManifest.cs new file mode 100644 index 0000000..c995f49 --- /dev/null +++ b/src/Meezi.Infrastructure/Data/MenuImageManifest.cs @@ -0,0 +1,101 @@ +using System.Text.Json; + +namespace Meezi.Infrastructure.Data; + +public static class MenuImageManifest +{ + private static IReadOnlyDictionary? _urls; + private static string? _defaultDrinkUrl; + private static string? _defaultFoodUrl; + + private const string FallbackDrinkUrl = + "https://images.unsplash.com/photo-1509042239860-f550ce710b93?w=600&auto=format&fit=crop"; + + private const string FallbackFoodUrl = + "https://images.unsplash.com/photo-1546793665-c74683f339c1?w=600"; + + public static string? GetImageUrl(string itemId) + { + EnsureLoaded(); + return _urls!.TryGetValue(itemId, out var url) ? url : null; + } + + public static string GetDefaultDrinkImageUrl() + { + EnsureLoaded(); + return _defaultDrinkUrl ?? FallbackDrinkUrl; + } + + public static string GetDefaultFoodImageUrl() + { + EnsureLoaded(); + return _defaultFoodUrl ?? FallbackFoodUrl; + } + + public static string ResolveImageUrl(string itemId, string fallback) + => GetLocalImageOverride(itemId) ?? fallback; + + /// Only imported Kaggle/upload paths override Food-101 fallbacks (not stale CDN URLs in manifest). + public static string? GetLocalImageOverride(string itemId) + { + var url = GetImageUrl(itemId); + if (string.IsNullOrWhiteSpace(url)) return null; + if (url.StartsWith("/uploads/", StringComparison.OrdinalIgnoreCase) + || url.StartsWith("uploads/", StringComparison.OrdinalIgnoreCase)) + return url.StartsWith('/') ? url : $"/{url}"; + return null; + } + + private static void EnsureLoaded() + { + if (_urls is not null) return; + LoadFromDisk(); + } + + private static void LoadFromDisk() + { + var paths = new[] + { + Path.Combine(AppContext.BaseDirectory, "data", "menu-image-manifest.json"), + Path.Combine(Directory.GetCurrentDirectory(), "data", "menu-image-manifest.json"), + Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "data", "menu-image-manifest.json")), + Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "data", "menu-image-manifest.json")), + }; + + foreach (var path in paths) + { + if (!File.Exists(path)) continue; + try + { + var json = File.ReadAllText(path); + var doc = JsonDocument.Parse(json); + if (doc.RootElement.TryGetProperty("defaults", out var defaults)) + { + if (defaults.TryGetProperty("drink", out var drink)) + _defaultDrinkUrl = drink.GetString(); + if (defaults.TryGetProperty("food", out var food)) + _defaultFoodUrl = food.GetString(); + } + + var map = new Dictionary(StringComparer.Ordinal); + if (doc.RootElement.TryGetProperty("items", out var items)) + { + foreach (var prop in items.EnumerateObject()) + { + if (prop.Value.TryGetProperty("imageUrl", out var url)) + map[prop.Name] = url.GetString() ?? ""; + } + } + + _urls = map; + return; + } + catch + { + // try next path + } + } + + _urls = new Dictionary(StringComparer.Ordinal); + } +} diff --git a/src/Meezi.Infrastructure/Data/MenuItemImageDefaults.cs b/src/Meezi.Infrastructure/Data/MenuItemImageDefaults.cs new file mode 100644 index 0000000..91ca58b --- /dev/null +++ b/src/Meezi.Infrastructure/Data/MenuItemImageDefaults.cs @@ -0,0 +1,67 @@ +using Meezi.Core.Entities; + +namespace Meezi.Infrastructure.Data; + +public enum MenuItemVisualKind +{ + Food, + Drink +} + +public static class MenuItemImageDefaults +{ + private static readonly HashSet DrinkCategoryIds = new(StringComparer.Ordinal) + { + "cat_demo_drinks", + "cat_demo_cold" + }; + + private static readonly string[] DrinkCategoryHints = + [ + "drink", "cold", "coffee", "tea", "juice", "smoothie", "beverage", "bar", + "نوشیدنی", "سرد", "گرم", "قهوه", "چای", "آبمیوه", "اسموتی", "مشروب", "بار" + ]; + + public static MenuItemVisualKind InferKind(string categoryId, string? categoryName = null) + { + if (DrinkCategoryIds.Contains(categoryId)) + return MenuItemVisualKind.Drink; + + var haystack = $"{categoryId} {categoryName}".ToLowerInvariant(); + if (DrinkCategoryHints.Any(h => haystack.Contains(h, StringComparison.Ordinal))) + return MenuItemVisualKind.Drink; + + return MenuItemVisualKind.Food; + } + + public static string GetDefaultImageUrl(MenuItemVisualKind kind) + => kind == MenuItemVisualKind.Drink + ? MenuImageManifest.GetDefaultDrinkImageUrl() + : MenuImageManifest.GetDefaultFoodImageUrl(); + + /// Remote https URL suitable for <img src> (excludes missing local uploads). + public static bool IsUsableImageUrl(string? imageUrl) => + !string.IsNullOrWhiteSpace(imageUrl) + && (imageUrl.StartsWith("https://", StringComparison.OrdinalIgnoreCase) + || imageUrl.StartsWith("http://", StringComparison.OrdinalIgnoreCase)); + + public static bool NeedsImageRepair(string? imageUrl) => !IsUsableImageUrl(imageUrl); + + public static string ResolveImageUrl(string itemId, string categoryId, string? categoryName) + { + var localOverride = MenuImageManifest.GetLocalImageOverride(itemId); + if (!string.IsNullOrWhiteSpace(localOverride)) + return localOverride; + + var catalog = DemoMenuCatalog.Items.FirstOrDefault(i => i.Id == itemId); + if (catalog is not null) + return DemoMenuCatalog.ResolveItemImageUrl(catalog); + + return GetDefaultImageUrl(InferKind(categoryId, categoryName)); + } + + public static string ResolveDisplayImageUrl(MenuItem item) => + IsUsableImageUrl(item.ImageUrl) + ? item.ImageUrl! + : ResolveImageUrl(item.Id, item.CategoryId, item.Category?.Name); +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260519200540_InitialCreate.Designer.cs b/src/Meezi.Infrastructure/Data/Migrations/20260519200540_InitialCreate.Designer.cs new file mode 100644 index 0000000..61e361c --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260519200540_InitialCreate.Designer.cs @@ -0,0 +1,1045 @@ +// +using System; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260519200540_InitialCreate")] + partial class InitialCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ClockIn") + .HasColumnType("timestamp with time zone"); + + b.Property("ClockOut") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "Date") + .IsUnique(); + + b.ToTable("Attendances"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Branches"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsVerified") + .HasColumnType("boolean"); + + b.Property("LogoUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("PlanExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("PreferredLanguage") + .IsRequired() + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Cafes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MaxDiscount") + .HasColumnType("numeric"); + + b.Property("MinOrderAmount") + .HasColumnType("numeric"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TargetGroup") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UsageLimit") + .HasColumnType("integer"); + + b.Property("UsedCount") + .HasColumnType("integer"); + + b.Property("Value") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Code") + .IsUnique(); + + b.ToTable("Coupons"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BirthDateJalali") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Group") + .HasColumnType("integer"); + + b.Property("LoyaltyPoints") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReferredBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("PinCode") + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Employees"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Deductions") + .HasColumnType("numeric"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsPaid") + .HasColumnType("boolean"); + + b.Property("MonthYear") + .IsRequired() + .HasColumnType("text"); + + b.Property("NetSalary") + .HasColumnType("numeric"); + + b.Property("OvertimePay") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "MonthYear") + .IsUnique(); + + b.ToTable("EmployeeSalaries"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndDate") + .HasColumnType("date"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("ReviewedBy") + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId"); + + b.ToTable("LeaveRequests"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("TaxId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("TaxId"); + + b.ToTable("MenuCategories"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CategoryId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("CategoryId"); + + b.ToTable("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CouponId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("EmployeeId") + .HasColumnType("text"); + + b.Property("OrderType") + .HasColumnType("integer"); + + b.Property("SnappfoodOrderId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subtotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("TaxTotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Total") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId"); + + b.HasIndex("CouponId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("TableId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UnitPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Reference") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DayOfWeek") + .HasColumnType("integer"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShiftType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "DayOfWeek") + .IsUnique(); + + b.ToTable("Shifts"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Floor") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Number") + .HasColumnType("integer"); + + b.Property("QrCode") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId"); + + b.HasIndex("QrCode") + .IsUnique(); + + b.ToTable("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsCompound") + .HasColumnType("boolean"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("IsRequired") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Attendances") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Branches") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Coupons") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Customers") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Employees") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Salaries") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("LeaveRequests") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuCategories") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Tax", "Tax") + .WithMany("MenuCategories") + .HasForeignKey("TaxId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Tax"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuItems") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuCategory", "Category") + .WithMany("MenuItems") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Orders") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Orders") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Coupon", "Coupon") + .WithMany("Orders") + .HasForeignKey("CouponId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Orders") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany("Orders") + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Coupon"); + + b.Navigation("Customer"); + + b.Navigation("Employee"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("OrderItems") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Items") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MenuItem"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Payments") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Shifts") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Tables") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Tables") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Taxes") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Navigation("Orders"); + + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Navigation("Branches"); + + b.Navigation("Coupons"); + + b.Navigation("Customers"); + + b.Navigation("Employees"); + + b.Navigation("MenuCategories"); + + b.Navigation("MenuItems"); + + b.Navigation("Orders"); + + b.Navigation("Tables"); + + b.Navigation("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Navigation("Attendances"); + + b.Navigation("LeaveRequests"); + + b.Navigation("Orders"); + + b.Navigation("Salaries"); + + b.Navigation("Shifts"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Navigation("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Navigation("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Navigation("Items"); + + b.Navigation("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Navigation("MenuCategories"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260519200540_InitialCreate.cs b/src/Meezi.Infrastructure/Data/Migrations/20260519200540_InitialCreate.cs new file mode 100644 index 0000000..c8efa97 --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260519200540_InitialCreate.cs @@ -0,0 +1,672 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Cafes", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + Name = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + NameAr = table.Column(type: "text", nullable: true), + NameEn = table.Column(type: "text", nullable: true), + Slug = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + Phone = table.Column(type: "text", nullable: true), + Address = table.Column(type: "text", nullable: true), + City = table.Column(type: "text", nullable: true), + LogoUrl = table.Column(type: "text", nullable: true), + PlanTier = table.Column(type: "integer", nullable: false), + PlanExpiresAt = table.Column(type: "timestamp with time zone", nullable: true), + IsVerified = table.Column(type: "boolean", nullable: false), + PreferredLanguage = table.Column(type: "text", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Cafes", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Branches", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + Name = table.Column(type: "text", nullable: false), + Address = table.Column(type: "text", nullable: true), + City = table.Column(type: "text", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CafeId = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Branches", x => x.Id); + table.ForeignKey( + name: "FK_Branches_Cafes_CafeId", + column: x => x.CafeId, + principalTable: "Cafes", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Coupons", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + Code = table.Column(type: "text", nullable: false), + Type = table.Column(type: "integer", nullable: false), + Value = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + MinOrderAmount = table.Column(type: "numeric", nullable: true), + MaxDiscount = table.Column(type: "numeric", nullable: true), + UsageLimit = table.Column(type: "integer", nullable: true), + UsedCount = table.Column(type: "integer", nullable: false), + TargetGroup = table.Column(type: "integer", nullable: true), + StartsAt = table.Column(type: "timestamp with time zone", nullable: true), + ExpiresAt = table.Column(type: "timestamp with time zone", nullable: true), + IsActive = table.Column(type: "boolean", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CafeId = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Coupons", x => x.Id); + table.ForeignKey( + name: "FK_Coupons_Cafes_CafeId", + column: x => x.CafeId, + principalTable: "Cafes", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Customers", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + Name = table.Column(type: "text", nullable: false), + Phone = table.Column(type: "text", nullable: false), + NationalId = table.Column(type: "text", nullable: true), + BirthDateJalali = table.Column(type: "text", nullable: true), + Group = table.Column(type: "integer", nullable: false), + LoyaltyPoints = table.Column(type: "integer", nullable: false), + ReferredBy = table.Column(type: "text", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CafeId = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Customers", x => x.Id); + table.ForeignKey( + name: "FK_Customers_Cafes_CafeId", + column: x => x.CafeId, + principalTable: "Cafes", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Employees", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + Name = table.Column(type: "text", nullable: false), + Phone = table.Column(type: "text", nullable: false), + NationalId = table.Column(type: "text", nullable: true), + Role = table.Column(type: "integer", nullable: false), + BaseSalary = table.Column(type: "numeric", nullable: false), + PinCode = table.Column(type: "text", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CafeId = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Employees", x => x.Id); + table.ForeignKey( + name: "FK_Employees_Cafes_CafeId", + column: x => x.CafeId, + principalTable: "Cafes", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Taxes", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + Name = table.Column(type: "text", nullable: false), + Rate = table.Column(type: "numeric(5,2)", precision: 5, scale: 2, nullable: false), + IsDefault = table.Column(type: "boolean", nullable: false), + IsRequired = table.Column(type: "boolean", nullable: false), + IsCompound = table.Column(type: "boolean", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CafeId = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Taxes", x => x.Id); + table.ForeignKey( + name: "FK_Taxes_Cafes_CafeId", + column: x => x.CafeId, + principalTable: "Cafes", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Tables", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + BranchId = table.Column(type: "text", nullable: true), + Number = table.Column(type: "integer", nullable: false), + Capacity = table.Column(type: "integer", nullable: false), + Floor = table.Column(type: "text", nullable: true), + QrCode = table.Column(type: "text", nullable: false), + IsActive = table.Column(type: "boolean", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CafeId = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Tables", x => x.Id); + table.ForeignKey( + name: "FK_Tables_Branches_BranchId", + column: x => x.BranchId, + principalTable: "Branches", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "FK_Tables_Cafes_CafeId", + column: x => x.CafeId, + principalTable: "Cafes", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Attendances", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + EmployeeId = table.Column(type: "text", nullable: false), + Date = table.Column(type: "date", nullable: false), + ClockIn = table.Column(type: "timestamp with time zone", nullable: true), + ClockOut = table.Column(type: "timestamp with time zone", nullable: true), + Notes = table.Column(type: "text", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Attendances", x => x.Id); + table.ForeignKey( + name: "FK_Attendances_Employees_EmployeeId", + column: x => x.EmployeeId, + principalTable: "Employees", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "EmployeeSalaries", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + EmployeeId = table.Column(type: "text", nullable: false), + MonthYear = table.Column(type: "text", nullable: false), + BaseSalary = table.Column(type: "numeric", nullable: false), + OvertimePay = table.Column(type: "numeric", nullable: false), + Deductions = table.Column(type: "numeric", nullable: false), + NetSalary = table.Column(type: "numeric", nullable: false), + IsPaid = table.Column(type: "boolean", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_EmployeeSalaries", x => x.Id); + table.ForeignKey( + name: "FK_EmployeeSalaries_Employees_EmployeeId", + column: x => x.EmployeeId, + principalTable: "Employees", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "LeaveRequests", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + EmployeeId = table.Column(type: "text", nullable: false), + StartDate = table.Column(type: "date", nullable: false), + EndDate = table.Column(type: "date", nullable: false), + Reason = table.Column(type: "text", nullable: true), + Status = table.Column(type: "integer", nullable: false), + ReviewedBy = table.Column(type: "text", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_LeaveRequests", x => x.Id); + table.ForeignKey( + name: "FK_LeaveRequests_Employees_EmployeeId", + column: x => x.EmployeeId, + principalTable: "Employees", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Shifts", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + EmployeeId = table.Column(type: "text", nullable: false), + DayOfWeek = table.Column(type: "integer", nullable: false), + ShiftType = table.Column(type: "integer", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Shifts", x => x.Id); + table.ForeignKey( + name: "FK_Shifts_Employees_EmployeeId", + column: x => x.EmployeeId, + principalTable: "Employees", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "MenuCategories", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + Name = table.Column(type: "text", nullable: false), + NameAr = table.Column(type: "text", nullable: true), + NameEn = table.Column(type: "text", nullable: true), + SortOrder = table.Column(type: "integer", nullable: false), + TaxId = table.Column(type: "text", nullable: true), + DiscountPercent = table.Column(type: "numeric", nullable: false), + IsActive = table.Column(type: "boolean", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CafeId = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_MenuCategories", x => x.Id); + table.ForeignKey( + name: "FK_MenuCategories_Cafes_CafeId", + column: x => x.CafeId, + principalTable: "Cafes", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_MenuCategories_Taxes_TaxId", + column: x => x.TaxId, + principalTable: "Taxes", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateTable( + name: "Orders", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + BranchId = table.Column(type: "text", nullable: true), + TableId = table.Column(type: "text", nullable: true), + CustomerId = table.Column(type: "text", nullable: true), + EmployeeId = table.Column(type: "text", nullable: true), + OrderType = table.Column(type: "integer", nullable: false), + Status = table.Column(type: "integer", nullable: false), + CouponId = table.Column(type: "text", nullable: true), + DiscountAmount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + Subtotal = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + TaxTotal = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + Total = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + SnappfoodOrderId = table.Column(type: "text", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CafeId = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Orders", x => x.Id); + table.ForeignKey( + name: "FK_Orders_Branches_BranchId", + column: x => x.BranchId, + principalTable: "Branches", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "FK_Orders_Cafes_CafeId", + column: x => x.CafeId, + principalTable: "Cafes", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Orders_Coupons_CouponId", + column: x => x.CouponId, + principalTable: "Coupons", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "FK_Orders_Customers_CustomerId", + column: x => x.CustomerId, + principalTable: "Customers", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "FK_Orders_Employees_EmployeeId", + column: x => x.EmployeeId, + principalTable: "Employees", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "FK_Orders_Tables_TableId", + column: x => x.TableId, + principalTable: "Tables", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateTable( + name: "MenuItems", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + CategoryId = table.Column(type: "text", nullable: false), + Name = table.Column(type: "text", nullable: false), + NameAr = table.Column(type: "text", nullable: true), + NameEn = table.Column(type: "text", nullable: true), + Description = table.Column(type: "text", nullable: true), + Price = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + ImageUrl = table.Column(type: "text", nullable: true), + IsAvailable = table.Column(type: "boolean", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CafeId = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_MenuItems", x => x.Id); + table.ForeignKey( + name: "FK_MenuItems_Cafes_CafeId", + column: x => x.CafeId, + principalTable: "Cafes", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_MenuItems_MenuCategories_CategoryId", + column: x => x.CategoryId, + principalTable: "MenuCategories", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Payments", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + OrderId = table.Column(type: "text", nullable: false), + Method = table.Column(type: "integer", nullable: false), + Amount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + Status = table.Column(type: "integer", nullable: false), + Reference = table.Column(type: "text", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Payments", x => x.Id); + table.ForeignKey( + name: "FK_Payments_Orders_OrderId", + column: x => x.OrderId, + principalTable: "Orders", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "OrderItems", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + OrderId = table.Column(type: "text", nullable: false), + MenuItemId = table.Column(type: "text", nullable: false), + Quantity = table.Column(type: "integer", nullable: false), + UnitPrice = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + Notes = table.Column(type: "text", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_OrderItems", x => x.Id); + table.ForeignKey( + name: "FK_OrderItems_MenuItems_MenuItemId", + column: x => x.MenuItemId, + principalTable: "MenuItems", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_OrderItems_Orders_OrderId", + column: x => x.OrderId, + principalTable: "Orders", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Attendances_EmployeeId_Date", + table: "Attendances", + columns: new[] { "EmployeeId", "Date" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Branches_CafeId", + table: "Branches", + column: "CafeId"); + + migrationBuilder.CreateIndex( + name: "IX_Cafes_Slug", + table: "Cafes", + column: "Slug", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Coupons_CafeId_Code", + table: "Coupons", + columns: new[] { "CafeId", "Code" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Customers_CafeId_Phone", + table: "Customers", + columns: new[] { "CafeId", "Phone" }); + + migrationBuilder.CreateIndex( + name: "IX_Employees_CafeId_Phone", + table: "Employees", + columns: new[] { "CafeId", "Phone" }); + + migrationBuilder.CreateIndex( + name: "IX_EmployeeSalaries_EmployeeId_MonthYear", + table: "EmployeeSalaries", + columns: new[] { "EmployeeId", "MonthYear" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_LeaveRequests_EmployeeId", + table: "LeaveRequests", + column: "EmployeeId"); + + migrationBuilder.CreateIndex( + name: "IX_MenuCategories_CafeId", + table: "MenuCategories", + column: "CafeId"); + + migrationBuilder.CreateIndex( + name: "IX_MenuCategories_TaxId", + table: "MenuCategories", + column: "TaxId"); + + migrationBuilder.CreateIndex( + name: "IX_MenuItems_CafeId", + table: "MenuItems", + column: "CafeId"); + + migrationBuilder.CreateIndex( + name: "IX_MenuItems_CategoryId", + table: "MenuItems", + column: "CategoryId"); + + migrationBuilder.CreateIndex( + name: "IX_OrderItems_MenuItemId", + table: "OrderItems", + column: "MenuItemId"); + + migrationBuilder.CreateIndex( + name: "IX_OrderItems_OrderId", + table: "OrderItems", + column: "OrderId"); + + migrationBuilder.CreateIndex( + name: "IX_Orders_BranchId", + table: "Orders", + column: "BranchId"); + + migrationBuilder.CreateIndex( + name: "IX_Orders_CafeId", + table: "Orders", + column: "CafeId"); + + migrationBuilder.CreateIndex( + name: "IX_Orders_CouponId", + table: "Orders", + column: "CouponId"); + + migrationBuilder.CreateIndex( + name: "IX_Orders_CustomerId", + table: "Orders", + column: "CustomerId"); + + migrationBuilder.CreateIndex( + name: "IX_Orders_EmployeeId", + table: "Orders", + column: "EmployeeId"); + + migrationBuilder.CreateIndex( + name: "IX_Orders_TableId", + table: "Orders", + column: "TableId"); + + migrationBuilder.CreateIndex( + name: "IX_Payments_OrderId", + table: "Payments", + column: "OrderId"); + + migrationBuilder.CreateIndex( + name: "IX_Shifts_EmployeeId_DayOfWeek", + table: "Shifts", + columns: new[] { "EmployeeId", "DayOfWeek" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Tables_BranchId", + table: "Tables", + column: "BranchId"); + + migrationBuilder.CreateIndex( + name: "IX_Tables_CafeId", + table: "Tables", + column: "CafeId"); + + migrationBuilder.CreateIndex( + name: "IX_Tables_QrCode", + table: "Tables", + column: "QrCode", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Taxes_CafeId", + table: "Taxes", + column: "CafeId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Attendances"); + + migrationBuilder.DropTable( + name: "EmployeeSalaries"); + + migrationBuilder.DropTable( + name: "LeaveRequests"); + + migrationBuilder.DropTable( + name: "OrderItems"); + + migrationBuilder.DropTable( + name: "Payments"); + + migrationBuilder.DropTable( + name: "Shifts"); + + migrationBuilder.DropTable( + name: "MenuItems"); + + migrationBuilder.DropTable( + name: "Orders"); + + migrationBuilder.DropTable( + name: "MenuCategories"); + + migrationBuilder.DropTable( + name: "Coupons"); + + migrationBuilder.DropTable( + name: "Customers"); + + migrationBuilder.DropTable( + name: "Employees"); + + migrationBuilder.DropTable( + name: "Tables"); + + migrationBuilder.DropTable( + name: "Taxes"); + + migrationBuilder.DropTable( + name: "Branches"); + + migrationBuilder.DropTable( + name: "Cafes"); + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260520010219_AddTableReservations.Designer.cs b/src/Meezi.Infrastructure/Data/Migrations/20260520010219_AddTableReservations.Designer.cs new file mode 100644 index 0000000..673720b --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260520010219_AddTableReservations.Designer.cs @@ -0,0 +1,1113 @@ +// +using System; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260520010219_AddTableReservations")] + partial class AddTableReservations + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ClockIn") + .HasColumnType("timestamp with time zone"); + + b.Property("ClockOut") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "Date") + .IsUnique(); + + b.ToTable("Attendances"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Branches"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsVerified") + .HasColumnType("boolean"); + + b.Property("LogoUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("PlanExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("PreferredLanguage") + .IsRequired() + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Cafes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MaxDiscount") + .HasColumnType("numeric"); + + b.Property("MinOrderAmount") + .HasColumnType("numeric"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TargetGroup") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UsageLimit") + .HasColumnType("integer"); + + b.Property("UsedCount") + .HasColumnType("integer"); + + b.Property("Value") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Code") + .IsUnique(); + + b.ToTable("Coupons"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BirthDateJalali") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Group") + .HasColumnType("integer"); + + b.Property("LoyaltyPoints") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReferredBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("PinCode") + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Employees"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Deductions") + .HasColumnType("numeric"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsPaid") + .HasColumnType("boolean"); + + b.Property("MonthYear") + .IsRequired() + .HasColumnType("text"); + + b.Property("NetSalary") + .HasColumnType("numeric"); + + b.Property("OvertimePay") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "MonthYear") + .IsUnique(); + + b.ToTable("EmployeeSalaries"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndDate") + .HasColumnType("date"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("ReviewedBy") + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId"); + + b.ToTable("LeaveRequests"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("TaxId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("TaxId"); + + b.ToTable("MenuCategories"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CategoryId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("CategoryId"); + + b.ToTable("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CouponId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("EmployeeId") + .HasColumnType("text"); + + b.Property("OrderType") + .HasColumnType("integer"); + + b.Property("SnappfoodOrderId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subtotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("TaxTotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Total") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId"); + + b.HasIndex("CouponId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("TableId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UnitPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Reference") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DayOfWeek") + .HasColumnType("integer"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShiftType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "DayOfWeek") + .IsUnique(); + + b.ToTable("Shifts"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Floor") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Number") + .HasColumnType("integer"); + + b.Property("QrCode") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId"); + + b.HasIndex("QrCode") + .IsUnique(); + + b.ToTable("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GuestName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GuestPhone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("PartySize") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Time") + .HasColumnType("time without time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("CafeId", "Date", "Time"); + + b.ToTable("TableReservations"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsCompound") + .HasColumnType("boolean"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("IsRequired") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Attendances") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Branches") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Coupons") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Customers") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Employees") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Salaries") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("LeaveRequests") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuCategories") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Tax", "Tax") + .WithMany("MenuCategories") + .HasForeignKey("TaxId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Tax"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuItems") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuCategory", "Category") + .WithMany("MenuItems") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Orders") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Orders") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Coupon", "Coupon") + .WithMany("Orders") + .HasForeignKey("CouponId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Orders") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany("Orders") + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Coupon"); + + b.Navigation("Customer"); + + b.Navigation("Employee"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("OrderItems") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Items") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MenuItem"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Payments") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Shifts") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Tables") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Tables") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Taxes") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Navigation("Orders"); + + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Navigation("Branches"); + + b.Navigation("Coupons"); + + b.Navigation("Customers"); + + b.Navigation("Employees"); + + b.Navigation("MenuCategories"); + + b.Navigation("MenuItems"); + + b.Navigation("Orders"); + + b.Navigation("Tables"); + + b.Navigation("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Navigation("Attendances"); + + b.Navigation("LeaveRequests"); + + b.Navigation("Orders"); + + b.Navigation("Salaries"); + + b.Navigation("Shifts"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Navigation("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Navigation("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Navigation("Items"); + + b.Navigation("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Navigation("MenuCategories"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260520010219_AddTableReservations.cs b/src/Meezi.Infrastructure/Data/Migrations/20260520010219_AddTableReservations.cs new file mode 100644 index 0000000..8746825 --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260520010219_AddTableReservations.cs @@ -0,0 +1,66 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + /// + public partial class AddTableReservations : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "TableReservations", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + CafeId = table.Column(type: "text", nullable: false), + CustomerId = table.Column(type: "text", nullable: true), + GuestName = table.Column(type: "text", nullable: false), + GuestPhone = table.Column(type: "text", nullable: false), + Date = table.Column(type: "date", nullable: false), + Time = table.Column(type: "time without time zone", nullable: false), + PartySize = table.Column(type: "integer", nullable: false), + Status = table.Column(type: "integer", nullable: false), + Notes = table.Column(type: "text", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_TableReservations", x => x.Id); + table.ForeignKey( + name: "FK_TableReservations_Cafes_CafeId", + column: x => x.CafeId, + principalTable: "Cafes", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_TableReservations_Customers_CustomerId", + column: x => x.CustomerId, + principalTable: "Customers", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateIndex( + name: "IX_TableReservations_CafeId_Date_Time", + table: "TableReservations", + columns: new[] { "CafeId", "Date", "Time" }); + + migrationBuilder.CreateIndex( + name: "IX_TableReservations_CustomerId", + table: "TableReservations", + column: "CustomerId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "TableReservations"); + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260520011003_AddCafeReviews.Designer.cs b/src/Meezi.Infrastructure/Data/Migrations/20260520011003_AddCafeReviews.Designer.cs new file mode 100644 index 0000000..7d1f30b --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260520011003_AddCafeReviews.Designer.cs @@ -0,0 +1,1174 @@ +// +using System; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260520011003_AddCafeReviews")] + partial class AddCafeReviews + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ClockIn") + .HasColumnType("timestamp with time zone"); + + b.Property("ClockOut") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "Date") + .IsUnique(); + + b.ToTable("Attendances"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Branches"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("CoverImageUrl") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsVerified") + .HasColumnType("boolean"); + + b.Property("LogoUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("PlanExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("PreferredLanguage") + .IsRequired() + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Cafes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AuthorName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("AuthorPhone") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Comment") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerRepliedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerReply") + .HasColumnType("text"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "CreatedAt"); + + b.ToTable("CafeReviews"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MaxDiscount") + .HasColumnType("numeric"); + + b.Property("MinOrderAmount") + .HasColumnType("numeric"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TargetGroup") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UsageLimit") + .HasColumnType("integer"); + + b.Property("UsedCount") + .HasColumnType("integer"); + + b.Property("Value") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Code") + .IsUnique(); + + b.ToTable("Coupons"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BirthDateJalali") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Group") + .HasColumnType("integer"); + + b.Property("LoyaltyPoints") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReferredBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("PinCode") + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Employees"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Deductions") + .HasColumnType("numeric"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsPaid") + .HasColumnType("boolean"); + + b.Property("MonthYear") + .IsRequired() + .HasColumnType("text"); + + b.Property("NetSalary") + .HasColumnType("numeric"); + + b.Property("OvertimePay") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "MonthYear") + .IsUnique(); + + b.ToTable("EmployeeSalaries"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndDate") + .HasColumnType("date"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("ReviewedBy") + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId"); + + b.ToTable("LeaveRequests"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("TaxId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("TaxId"); + + b.ToTable("MenuCategories"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CategoryId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("CategoryId"); + + b.ToTable("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CouponId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("EmployeeId") + .HasColumnType("text"); + + b.Property("OrderType") + .HasColumnType("integer"); + + b.Property("SnappfoodOrderId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subtotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("TaxTotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Total") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId"); + + b.HasIndex("CouponId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("TableId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UnitPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Reference") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DayOfWeek") + .HasColumnType("integer"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShiftType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "DayOfWeek") + .IsUnique(); + + b.ToTable("Shifts"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Floor") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Number") + .HasColumnType("integer"); + + b.Property("QrCode") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId"); + + b.HasIndex("QrCode") + .IsUnique(); + + b.ToTable("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GuestName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GuestPhone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("PartySize") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Time") + .HasColumnType("time without time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("CafeId", "Date", "Time"); + + b.ToTable("TableReservations"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsCompound") + .HasColumnType("boolean"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("IsRequired") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Attendances") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Branches") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Reviews") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Coupons") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Customers") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Employees") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Salaries") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("LeaveRequests") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuCategories") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Tax", "Tax") + .WithMany("MenuCategories") + .HasForeignKey("TaxId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Tax"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuItems") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuCategory", "Category") + .WithMany("MenuItems") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Orders") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Orders") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Coupon", "Coupon") + .WithMany("Orders") + .HasForeignKey("CouponId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Orders") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany("Orders") + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Coupon"); + + b.Navigation("Customer"); + + b.Navigation("Employee"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("OrderItems") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Items") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MenuItem"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Payments") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Shifts") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Tables") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Tables") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Taxes") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Navigation("Orders"); + + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Navigation("Branches"); + + b.Navigation("Coupons"); + + b.Navigation("Customers"); + + b.Navigation("Employees"); + + b.Navigation("MenuCategories"); + + b.Navigation("MenuItems"); + + b.Navigation("Orders"); + + b.Navigation("Reviews"); + + b.Navigation("Tables"); + + b.Navigation("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Navigation("Attendances"); + + b.Navigation("LeaveRequests"); + + b.Navigation("Orders"); + + b.Navigation("Salaries"); + + b.Navigation("Shifts"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Navigation("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Navigation("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Navigation("Items"); + + b.Navigation("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Navigation("MenuCategories"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260520011003_AddCafeReviews.cs b/src/Meezi.Infrastructure/Data/Migrations/20260520011003_AddCafeReviews.cs new file mode 100644 index 0000000..2f9bb86 --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260520011003_AddCafeReviews.cs @@ -0,0 +1,73 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + /// + public partial class AddCafeReviews : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "CoverImageUrl", + table: "Cafes", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "Description", + table: "Cafes", + type: "text", + nullable: true); + + migrationBuilder.CreateTable( + name: "CafeReviews", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + CafeId = table.Column(type: "text", nullable: false), + AuthorName = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + AuthorPhone = table.Column(type: "text", nullable: true), + Rating = table.Column(type: "integer", nullable: false), + Comment = table.Column(type: "text", nullable: true), + OwnerReply = table.Column(type: "text", nullable: true), + OwnerRepliedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_CafeReviews", x => x.Id); + table.ForeignKey( + name: "FK_CafeReviews_Cafes_CafeId", + column: x => x.CafeId, + principalTable: "Cafes", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_CafeReviews_CafeId_CreatedAt", + table: "CafeReviews", + columns: new[] { "CafeId", "CreatedAt" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "CafeReviews"); + + migrationBuilder.DropColumn( + name: "CoverImageUrl", + table: "Cafes"); + + migrationBuilder.DropColumn( + name: "Description", + table: "Cafes"); + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260520012452_AddSubscriptionBilling.Designer.cs b/src/Meezi.Infrastructure/Data/Migrations/20260520012452_AddSubscriptionBilling.Designer.cs new file mode 100644 index 0000000..2b57695 --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260520012452_AddSubscriptionBilling.Designer.cs @@ -0,0 +1,1237 @@ +// +using System; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260520012452_AddSubscriptionBilling")] + partial class AddSubscriptionBilling + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ClockIn") + .HasColumnType("timestamp with time zone"); + + b.Property("ClockOut") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "Date") + .IsUnique(); + + b.ToTable("Attendances"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Branches"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("CoverImageUrl") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsVerified") + .HasColumnType("boolean"); + + b.Property("LogoUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("PlanExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("PreferredLanguage") + .IsRequired() + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SnappfoodVendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Cafes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AuthorName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("AuthorPhone") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Comment") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerRepliedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerReply") + .HasColumnType("text"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "CreatedAt"); + + b.ToTable("CafeReviews"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MaxDiscount") + .HasColumnType("numeric"); + + b.Property("MinOrderAmount") + .HasColumnType("numeric"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TargetGroup") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UsageLimit") + .HasColumnType("integer"); + + b.Property("UsedCount") + .HasColumnType("integer"); + + b.Property("Value") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Code") + .IsUnique(); + + b.ToTable("Coupons"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BirthDateJalali") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Group") + .HasColumnType("integer"); + + b.Property("LoyaltyPoints") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReferredBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("PinCode") + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Employees"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Deductions") + .HasColumnType("numeric"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsPaid") + .HasColumnType("boolean"); + + b.Property("MonthYear") + .IsRequired() + .HasColumnType("text"); + + b.Property("NetSalary") + .HasColumnType("numeric"); + + b.Property("OvertimePay") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "MonthYear") + .IsUnique(); + + b.ToTable("EmployeeSalaries"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndDate") + .HasColumnType("date"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("ReviewedBy") + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId"); + + b.ToTable("LeaveRequests"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("TaxId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("TaxId"); + + b.ToTable("MenuCategories"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CategoryId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("CategoryId"); + + b.ToTable("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CouponId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("EmployeeId") + .HasColumnType("text"); + + b.Property("OrderType") + .HasColumnType("integer"); + + b.Property("SnappfoodOrderId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subtotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("TaxTotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Total") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId"); + + b.HasIndex("CouponId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("TableId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UnitPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Reference") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DayOfWeek") + .HasColumnType("integer"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShiftType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "DayOfWeek") + .IsUnique(); + + b.ToTable("Shifts"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AmountRials") + .HasColumnType("bigint"); + + b.Property("AmountToman") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Authority") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Months") + .HasColumnType("integer"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("RefId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Authority"); + + b.HasIndex("CafeId"); + + b.ToTable("SubscriptionPayments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Floor") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Number") + .HasColumnType("integer"); + + b.Property("QrCode") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId"); + + b.HasIndex("QrCode") + .IsUnique(); + + b.ToTable("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GuestName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GuestPhone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("PartySize") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Time") + .HasColumnType("time without time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("CafeId", "Date", "Time"); + + b.ToTable("TableReservations"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsCompound") + .HasColumnType("boolean"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("IsRequired") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Attendances") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Branches") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Reviews") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Coupons") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Customers") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Employees") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Salaries") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("LeaveRequests") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuCategories") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Tax", "Tax") + .WithMany("MenuCategories") + .HasForeignKey("TaxId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Tax"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuItems") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuCategory", "Category") + .WithMany("MenuItems") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Orders") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Orders") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Coupon", "Coupon") + .WithMany("Orders") + .HasForeignKey("CouponId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Orders") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany("Orders") + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Coupon"); + + b.Navigation("Customer"); + + b.Navigation("Employee"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("OrderItems") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Items") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MenuItem"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Payments") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Shifts") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("SubscriptionPayments") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Tables") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Tables") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Taxes") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Navigation("Orders"); + + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Navigation("Branches"); + + b.Navigation("Coupons"); + + b.Navigation("Customers"); + + b.Navigation("Employees"); + + b.Navigation("MenuCategories"); + + b.Navigation("MenuItems"); + + b.Navigation("Orders"); + + b.Navigation("Reviews"); + + b.Navigation("SubscriptionPayments"); + + b.Navigation("Tables"); + + b.Navigation("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Navigation("Attendances"); + + b.Navigation("LeaveRequests"); + + b.Navigation("Orders"); + + b.Navigation("Salaries"); + + b.Navigation("Shifts"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Navigation("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Navigation("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Navigation("Items"); + + b.Navigation("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Navigation("MenuCategories"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260520012452_AddSubscriptionBilling.cs b/src/Meezi.Infrastructure/Data/Migrations/20260520012452_AddSubscriptionBilling.cs new file mode 100644 index 0000000..f8f9a4b --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260520012452_AddSubscriptionBilling.cs @@ -0,0 +1,70 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + /// + public partial class AddSubscriptionBilling : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "SnappfoodVendorId", + table: "Cafes", + type: "character varying(100)", + maxLength: 100, + nullable: true); + + migrationBuilder.CreateTable( + name: "SubscriptionPayments", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + PlanTier = table.Column(type: "integer", nullable: false), + Months = table.Column(type: "integer", nullable: false), + AmountToman = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + AmountRials = table.Column(type: "bigint", nullable: false), + Authority = table.Column(type: "text", nullable: true), + RefId = table.Column(type: "text", nullable: true), + Status = table.Column(type: "integer", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CafeId = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_SubscriptionPayments", x => x.Id); + table.ForeignKey( + name: "FK_SubscriptionPayments_Cafes_CafeId", + column: x => x.CafeId, + principalTable: "Cafes", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_SubscriptionPayments_Authority", + table: "SubscriptionPayments", + column: "Authority"); + + migrationBuilder.CreateIndex( + name: "IX_SubscriptionPayments_CafeId", + table: "SubscriptionPayments", + column: "CafeId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "SubscriptionPayments"); + + migrationBuilder.DropColumn( + name: "SnappfoodVendorId", + table: "Cafes"); + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260520013656_TableManagementEnhancements.Designer.cs b/src/Meezi.Infrastructure/Data/Migrations/20260520013656_TableManagementEnhancements.Designer.cs new file mode 100644 index 0000000..50d6613 --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260520013656_TableManagementEnhancements.Designer.cs @@ -0,0 +1,1251 @@ +// +using System; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260520013656_TableManagementEnhancements")] + partial class TableManagementEnhancements + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ClockIn") + .HasColumnType("timestamp with time zone"); + + b.Property("ClockOut") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "Date") + .IsUnique(); + + b.ToTable("Attendances"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Branches"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("CoverImageUrl") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsVerified") + .HasColumnType("boolean"); + + b.Property("LogoUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("PlanExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("PreferredLanguage") + .IsRequired() + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SnappfoodVendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Cafes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AuthorName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("AuthorPhone") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Comment") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerRepliedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerReply") + .HasColumnType("text"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "CreatedAt"); + + b.ToTable("CafeReviews"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MaxDiscount") + .HasColumnType("numeric"); + + b.Property("MinOrderAmount") + .HasColumnType("numeric"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TargetGroup") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UsageLimit") + .HasColumnType("integer"); + + b.Property("UsedCount") + .HasColumnType("integer"); + + b.Property("Value") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Code") + .IsUnique(); + + b.ToTable("Coupons"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BirthDateJalali") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Group") + .HasColumnType("integer"); + + b.Property("LoyaltyPoints") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReferredBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("PinCode") + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Employees"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Deductions") + .HasColumnType("numeric"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsPaid") + .HasColumnType("boolean"); + + b.Property("MonthYear") + .IsRequired() + .HasColumnType("text"); + + b.Property("NetSalary") + .HasColumnType("numeric"); + + b.Property("OvertimePay") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "MonthYear") + .IsUnique(); + + b.ToTable("EmployeeSalaries"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndDate") + .HasColumnType("date"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("ReviewedBy") + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId"); + + b.ToTable("LeaveRequests"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("TaxId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("TaxId"); + + b.ToTable("MenuCategories"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CategoryId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("CategoryId"); + + b.ToTable("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CouponId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("EmployeeId") + .HasColumnType("text"); + + b.Property("OrderType") + .HasColumnType("integer"); + + b.Property("SnappfoodOrderId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subtotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("TaxTotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Total") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId"); + + b.HasIndex("CouponId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("TableId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UnitPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Reference") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DayOfWeek") + .HasColumnType("integer"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShiftType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "DayOfWeek") + .IsUnique(); + + b.ToTable("Shifts"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AmountRials") + .HasColumnType("bigint"); + + b.Property("AmountToman") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Authority") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Months") + .HasColumnType("integer"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("RefId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Authority"); + + b.HasIndex("CafeId"); + + b.ToTable("SubscriptionPayments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Floor") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Number") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("QrCode") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId"); + + b.HasIndex("QrCode") + .IsUnique(); + + b.ToTable("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GuestName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GuestPhone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("PartySize") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("time without time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("TableId"); + + b.HasIndex("CafeId", "Date", "Time"); + + b.ToTable("TableReservations"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsCompound") + .HasColumnType("boolean"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("IsRequired") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Attendances") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Branches") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Reviews") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Coupons") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Customers") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Employees") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Salaries") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("LeaveRequests") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuCategories") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Tax", "Tax") + .WithMany("MenuCategories") + .HasForeignKey("TaxId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Tax"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuItems") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuCategory", "Category") + .WithMany("MenuItems") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Orders") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Orders") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Coupon", "Coupon") + .WithMany("Orders") + .HasForeignKey("CouponId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Orders") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany("Orders") + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Coupon"); + + b.Navigation("Customer"); + + b.Navigation("Employee"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("OrderItems") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Items") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MenuItem"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Payments") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Shifts") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("SubscriptionPayments") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Tables") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Tables") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany() + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Customer"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Taxes") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Navigation("Orders"); + + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Navigation("Branches"); + + b.Navigation("Coupons"); + + b.Navigation("Customers"); + + b.Navigation("Employees"); + + b.Navigation("MenuCategories"); + + b.Navigation("MenuItems"); + + b.Navigation("Orders"); + + b.Navigation("Reviews"); + + b.Navigation("SubscriptionPayments"); + + b.Navigation("Tables"); + + b.Navigation("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Navigation("Attendances"); + + b.Navigation("LeaveRequests"); + + b.Navigation("Orders"); + + b.Navigation("Salaries"); + + b.Navigation("Shifts"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Navigation("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Navigation("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Navigation("Items"); + + b.Navigation("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Navigation("MenuCategories"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260520013656_TableManagementEnhancements.cs b/src/Meezi.Infrastructure/Data/Migrations/20260520013656_TableManagementEnhancements.cs new file mode 100644 index 0000000..e93418a --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260520013656_TableManagementEnhancements.cs @@ -0,0 +1,67 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + /// + public partial class TableManagementEnhancements : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Number", + table: "Tables", + type: "character varying(50)", + maxLength: 50, + nullable: false, + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AddColumn( + name: "TableId", + table: "TableReservations", + type: "text", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_TableReservations_TableId", + table: "TableReservations", + column: "TableId"); + + migrationBuilder.AddForeignKey( + name: "FK_TableReservations_Tables_TableId", + table: "TableReservations", + column: "TableId", + principalTable: "Tables", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_TableReservations_Tables_TableId", + table: "TableReservations"); + + migrationBuilder.DropIndex( + name: "IX_TableReservations_TableId", + table: "TableReservations"); + + migrationBuilder.DropColumn( + name: "TableId", + table: "TableReservations"); + + migrationBuilder.AlterColumn( + name: "Number", + table: "Tables", + type: "integer", + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(50)", + oldMaxLength: 50); + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260520022217_AddMenuItemDiscountAndMedia.Designer.cs b/src/Meezi.Infrastructure/Data/Migrations/20260520022217_AddMenuItemDiscountAndMedia.Designer.cs new file mode 100644 index 0000000..853cc00 --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260520022217_AddMenuItemDiscountAndMedia.Designer.cs @@ -0,0 +1,1254 @@ +// +using System; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260520022217_AddMenuItemDiscountAndMedia")] + partial class AddMenuItemDiscountAndMedia + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ClockIn") + .HasColumnType("timestamp with time zone"); + + b.Property("ClockOut") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "Date") + .IsUnique(); + + b.ToTable("Attendances"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Branches"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("CoverImageUrl") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsVerified") + .HasColumnType("boolean"); + + b.Property("LogoUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("PlanExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("PreferredLanguage") + .IsRequired() + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SnappfoodVendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Cafes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AuthorName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("AuthorPhone") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Comment") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerRepliedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerReply") + .HasColumnType("text"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "CreatedAt"); + + b.ToTable("CafeReviews"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MaxDiscount") + .HasColumnType("numeric"); + + b.Property("MinOrderAmount") + .HasColumnType("numeric"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TargetGroup") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UsageLimit") + .HasColumnType("integer"); + + b.Property("UsedCount") + .HasColumnType("integer"); + + b.Property("Value") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Code") + .IsUnique(); + + b.ToTable("Coupons"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BirthDateJalali") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Group") + .HasColumnType("integer"); + + b.Property("LoyaltyPoints") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReferredBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("PinCode") + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Employees"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Deductions") + .HasColumnType("numeric"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsPaid") + .HasColumnType("boolean"); + + b.Property("MonthYear") + .IsRequired() + .HasColumnType("text"); + + b.Property("NetSalary") + .HasColumnType("numeric"); + + b.Property("OvertimePay") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "MonthYear") + .IsUnique(); + + b.ToTable("EmployeeSalaries"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndDate") + .HasColumnType("date"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("ReviewedBy") + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId"); + + b.ToTable("LeaveRequests"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("TaxId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("TaxId"); + + b.ToTable("MenuCategories"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CategoryId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("CategoryId"); + + b.ToTable("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CouponId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("EmployeeId") + .HasColumnType("text"); + + b.Property("OrderType") + .HasColumnType("integer"); + + b.Property("SnappfoodOrderId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subtotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("TaxTotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Total") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId"); + + b.HasIndex("CouponId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("TableId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UnitPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Reference") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DayOfWeek") + .HasColumnType("integer"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShiftType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "DayOfWeek") + .IsUnique(); + + b.ToTable("Shifts"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AmountRials") + .HasColumnType("bigint"); + + b.Property("AmountToman") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Authority") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Months") + .HasColumnType("integer"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("RefId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Authority"); + + b.HasIndex("CafeId"); + + b.ToTable("SubscriptionPayments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Floor") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Number") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("QrCode") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId"); + + b.HasIndex("QrCode") + .IsUnique(); + + b.ToTable("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GuestName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GuestPhone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("PartySize") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("time without time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("TableId"); + + b.HasIndex("CafeId", "Date", "Time"); + + b.ToTable("TableReservations"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsCompound") + .HasColumnType("boolean"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("IsRequired") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Attendances") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Branches") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Reviews") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Coupons") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Customers") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Employees") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Salaries") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("LeaveRequests") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuCategories") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Tax", "Tax") + .WithMany("MenuCategories") + .HasForeignKey("TaxId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Tax"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuItems") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuCategory", "Category") + .WithMany("MenuItems") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Orders") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Orders") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Coupon", "Coupon") + .WithMany("Orders") + .HasForeignKey("CouponId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Orders") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany("Orders") + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Coupon"); + + b.Navigation("Customer"); + + b.Navigation("Employee"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("OrderItems") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Items") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MenuItem"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Payments") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Shifts") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("SubscriptionPayments") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Tables") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Tables") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany() + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Customer"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Taxes") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Navigation("Orders"); + + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Navigation("Branches"); + + b.Navigation("Coupons"); + + b.Navigation("Customers"); + + b.Navigation("Employees"); + + b.Navigation("MenuCategories"); + + b.Navigation("MenuItems"); + + b.Navigation("Orders"); + + b.Navigation("Reviews"); + + b.Navigation("SubscriptionPayments"); + + b.Navigation("Tables"); + + b.Navigation("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Navigation("Attendances"); + + b.Navigation("LeaveRequests"); + + b.Navigation("Orders"); + + b.Navigation("Salaries"); + + b.Navigation("Shifts"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Navigation("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Navigation("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Navigation("Items"); + + b.Navigation("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Navigation("MenuCategories"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260520022217_AddMenuItemDiscountAndMedia.cs b/src/Meezi.Infrastructure/Data/Migrations/20260520022217_AddMenuItemDiscountAndMedia.cs new file mode 100644 index 0000000..2e1a9b7 --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260520022217_AddMenuItemDiscountAndMedia.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + /// + public partial class AddMenuItemDiscountAndMedia : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "DiscountPercent", + table: "MenuItems", + type: "numeric", + nullable: false, + defaultValue: 0m); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "DiscountPercent", + table: "MenuItems"); + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260520030143_AddInventoryEntities.Designer.cs b/src/Meezi.Infrastructure/Data/Migrations/20260520030143_AddInventoryEntities.Designer.cs new file mode 100644 index 0000000..322392f --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260520030143_AddInventoryEntities.Designer.cs @@ -0,0 +1,1366 @@ +// +using System; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260520030143_AddInventoryEntities")] + partial class AddInventoryEntities + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ClockIn") + .HasColumnType("timestamp with time zone"); + + b.Property("ClockOut") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "Date") + .IsUnique(); + + b.ToTable("Attendances"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Branches"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("CoverImageUrl") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsVerified") + .HasColumnType("boolean"); + + b.Property("LogoUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("PlanExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("PreferredLanguage") + .IsRequired() + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SnappfoodVendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Cafes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AuthorName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("AuthorPhone") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Comment") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerRepliedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerReply") + .HasColumnType("text"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "CreatedAt"); + + b.ToTable("CafeReviews"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MaxDiscount") + .HasColumnType("numeric"); + + b.Property("MinOrderAmount") + .HasColumnType("numeric"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TargetGroup") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UsageLimit") + .HasColumnType("integer"); + + b.Property("UsedCount") + .HasColumnType("integer"); + + b.Property("Value") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Code") + .IsUnique(); + + b.ToTable("Coupons"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BirthDateJalali") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Group") + .HasColumnType("integer"); + + b.Property("LoyaltyPoints") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReferredBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("PinCode") + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Employees"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Deductions") + .HasColumnType("numeric"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsPaid") + .HasColumnType("boolean"); + + b.Property("MonthYear") + .IsRequired() + .HasColumnType("text"); + + b.Property("NetSalary") + .HasColumnType("numeric"); + + b.Property("OvertimePay") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "MonthYear") + .IsUnique(); + + b.ToTable("EmployeeSalaries"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("QuantityOnHand") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("ReorderLevel") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("Unit") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Ingredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndDate") + .HasColumnType("date"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("ReviewedBy") + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId"); + + b.ToTable("LeaveRequests"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("TaxId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("TaxId"); + + b.ToTable("MenuCategories"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CategoryId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("CategoryId"); + + b.ToTable("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CouponId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("EmployeeId") + .HasColumnType("text"); + + b.Property("OrderType") + .HasColumnType("integer"); + + b.Property("SnappfoodOrderId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subtotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("TaxTotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Total") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId"); + + b.HasIndex("CouponId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("TableId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UnitPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Reference") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DayOfWeek") + .HasColumnType("integer"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShiftType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "DayOfWeek") + .IsUnique(); + + b.ToTable("Shifts"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Delta") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("IngredientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Note") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("IngredientId"); + + b.ToTable("StockMovements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AmountRials") + .HasColumnType("bigint"); + + b.Property("AmountToman") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Authority") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Months") + .HasColumnType("integer"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("RefId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Authority"); + + b.HasIndex("CafeId"); + + b.ToTable("SubscriptionPayments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Floor") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Number") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("QrCode") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId"); + + b.HasIndex("QrCode") + .IsUnique(); + + b.ToTable("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GuestName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GuestPhone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("PartySize") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("time without time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("TableId"); + + b.HasIndex("CafeId", "Date", "Time"); + + b.ToTable("TableReservations"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsCompound") + .HasColumnType("boolean"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("IsRequired") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Attendances") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Branches") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Reviews") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Coupons") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Customers") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Employees") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Salaries") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Ingredients") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("LeaveRequests") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuCategories") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Tax", "Tax") + .WithMany("MenuCategories") + .HasForeignKey("TaxId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Tax"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuItems") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuCategory", "Category") + .WithMany("MenuItems") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Orders") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Orders") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Coupon", "Coupon") + .WithMany("Orders") + .HasForeignKey("CouponId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Orders") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany("Orders") + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Coupon"); + + b.Navigation("Customer"); + + b.Navigation("Employee"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("OrderItems") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Items") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MenuItem"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Payments") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Shifts") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Ingredient", "Ingredient") + .WithMany("Movements") + .HasForeignKey("IngredientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Ingredient"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("SubscriptionPayments") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Tables") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Tables") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany() + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Customer"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Taxes") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Navigation("Orders"); + + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Navigation("Branches"); + + b.Navigation("Coupons"); + + b.Navigation("Customers"); + + b.Navigation("Employees"); + + b.Navigation("Ingredients"); + + b.Navigation("MenuCategories"); + + b.Navigation("MenuItems"); + + b.Navigation("Orders"); + + b.Navigation("Reviews"); + + b.Navigation("SubscriptionPayments"); + + b.Navigation("Tables"); + + b.Navigation("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Navigation("Attendances"); + + b.Navigation("LeaveRequests"); + + b.Navigation("Orders"); + + b.Navigation("Salaries"); + + b.Navigation("Shifts"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Navigation("Movements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Navigation("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Navigation("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Navigation("Items"); + + b.Navigation("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Navigation("MenuCategories"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260520030143_AddInventoryEntities.cs b/src/Meezi.Infrastructure/Data/Migrations/20260520030143_AddInventoryEntities.cs new file mode 100644 index 0000000..002b3e5 --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260520030143_AddInventoryEntities.cs @@ -0,0 +1,93 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + /// + public partial class AddInventoryEntities : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Ingredients", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + Name = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + Unit = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + QuantityOnHand = table.Column(type: "numeric(18,3)", precision: 18, scale: 3, nullable: false), + ReorderLevel = table.Column(type: "numeric(18,3)", precision: 18, scale: 3, nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CafeId = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Ingredients", x => x.Id); + table.ForeignKey( + name: "FK_Ingredients_Cafes_CafeId", + column: x => x.CafeId, + principalTable: "Cafes", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "StockMovements", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + IngredientId = table.Column(type: "text", nullable: false), + Delta = table.Column(type: "numeric(18,3)", precision: 18, scale: 3, nullable: false), + Note = table.Column(type: "text", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CafeId = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_StockMovements", x => x.Id); + table.ForeignKey( + name: "FK_StockMovements_Cafes_CafeId", + column: x => x.CafeId, + principalTable: "Cafes", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_StockMovements_Ingredients_IngredientId", + column: x => x.IngredientId, + principalTable: "Ingredients", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Ingredients_CafeId", + table: "Ingredients", + column: "CafeId"); + + migrationBuilder.CreateIndex( + name: "IX_StockMovements_CafeId", + table: "StockMovements", + column: "CafeId"); + + migrationBuilder.CreateIndex( + name: "IX_StockMovements_IngredientId", + table: "StockMovements", + column: "IngredientId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "StockMovements"); + + migrationBuilder.DropTable( + name: "Ingredients"); + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260520032420_AddMenuAndTableMedia.Designer.cs b/src/Meezi.Infrastructure/Data/Migrations/20260520032420_AddMenuAndTableMedia.Designer.cs new file mode 100644 index 0000000..b29d91e --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260520032420_AddMenuAndTableMedia.Designer.cs @@ -0,0 +1,1375 @@ +// +using System; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260520032420_AddMenuAndTableMedia")] + partial class AddMenuAndTableMedia + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ClockIn") + .HasColumnType("timestamp with time zone"); + + b.Property("ClockOut") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "Date") + .IsUnique(); + + b.ToTable("Attendances"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Branches"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("CoverImageUrl") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsVerified") + .HasColumnType("boolean"); + + b.Property("LogoUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("PlanExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("PreferredLanguage") + .IsRequired() + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SnappfoodVendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Cafes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AuthorName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("AuthorPhone") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Comment") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerRepliedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerReply") + .HasColumnType("text"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "CreatedAt"); + + b.ToTable("CafeReviews"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MaxDiscount") + .HasColumnType("numeric"); + + b.Property("MinOrderAmount") + .HasColumnType("numeric"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TargetGroup") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UsageLimit") + .HasColumnType("integer"); + + b.Property("UsedCount") + .HasColumnType("integer"); + + b.Property("Value") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Code") + .IsUnique(); + + b.ToTable("Coupons"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BirthDateJalali") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Group") + .HasColumnType("integer"); + + b.Property("LoyaltyPoints") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReferredBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("PinCode") + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Employees"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Deductions") + .HasColumnType("numeric"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsPaid") + .HasColumnType("boolean"); + + b.Property("MonthYear") + .IsRequired() + .HasColumnType("text"); + + b.Property("NetSalary") + .HasColumnType("numeric"); + + b.Property("OvertimePay") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "MonthYear") + .IsUnique(); + + b.ToTable("EmployeeSalaries"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("QuantityOnHand") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("ReorderLevel") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("Unit") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Ingredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndDate") + .HasColumnType("date"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("ReviewedBy") + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId"); + + b.ToTable("LeaveRequests"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("TaxId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("TaxId"); + + b.ToTable("MenuCategories"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CategoryId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("CategoryId"); + + b.ToTable("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CouponId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("EmployeeId") + .HasColumnType("text"); + + b.Property("OrderType") + .HasColumnType("integer"); + + b.Property("SnappfoodOrderId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subtotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("TaxTotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Total") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId"); + + b.HasIndex("CouponId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("TableId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UnitPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Reference") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DayOfWeek") + .HasColumnType("integer"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShiftType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "DayOfWeek") + .IsUnique(); + + b.ToTable("Shifts"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Delta") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("IngredientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Note") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("IngredientId"); + + b.ToTable("StockMovements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AmountRials") + .HasColumnType("bigint"); + + b.Property("AmountToman") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Authority") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Months") + .HasColumnType("integer"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("RefId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Authority"); + + b.HasIndex("CafeId"); + + b.ToTable("SubscriptionPayments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Floor") + .HasColumnType("text"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Number") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("QrCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId"); + + b.HasIndex("QrCode") + .IsUnique(); + + b.ToTable("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GuestName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GuestPhone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("PartySize") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("time without time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("TableId"); + + b.HasIndex("CafeId", "Date", "Time"); + + b.ToTable("TableReservations"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsCompound") + .HasColumnType("boolean"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("IsRequired") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Attendances") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Branches") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Reviews") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Coupons") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Customers") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Employees") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Salaries") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Ingredients") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("LeaveRequests") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuCategories") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Tax", "Tax") + .WithMany("MenuCategories") + .HasForeignKey("TaxId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Tax"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuItems") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuCategory", "Category") + .WithMany("MenuItems") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Orders") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Orders") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Coupon", "Coupon") + .WithMany("Orders") + .HasForeignKey("CouponId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Orders") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany("Orders") + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Coupon"); + + b.Navigation("Customer"); + + b.Navigation("Employee"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("OrderItems") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Items") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MenuItem"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Payments") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Shifts") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Ingredient", "Ingredient") + .WithMany("Movements") + .HasForeignKey("IngredientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Ingredient"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("SubscriptionPayments") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Tables") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Tables") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany() + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Customer"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Taxes") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Navigation("Orders"); + + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Navigation("Branches"); + + b.Navigation("Coupons"); + + b.Navigation("Customers"); + + b.Navigation("Employees"); + + b.Navigation("Ingredients"); + + b.Navigation("MenuCategories"); + + b.Navigation("MenuItems"); + + b.Navigation("Orders"); + + b.Navigation("Reviews"); + + b.Navigation("SubscriptionPayments"); + + b.Navigation("Tables"); + + b.Navigation("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Navigation("Attendances"); + + b.Navigation("LeaveRequests"); + + b.Navigation("Orders"); + + b.Navigation("Salaries"); + + b.Navigation("Shifts"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Navigation("Movements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Navigation("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Navigation("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Navigation("Items"); + + b.Navigation("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Navigation("MenuCategories"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260520032420_AddMenuAndTableMedia.cs b/src/Meezi.Infrastructure/Data/Migrations/20260520032420_AddMenuAndTableMedia.cs new file mode 100644 index 0000000..0a70e97 --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260520032420_AddMenuAndTableMedia.cs @@ -0,0 +1,48 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + /// + public partial class AddMenuAndTableMedia : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ImageUrl", + table: "Tables", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "VideoUrl", + table: "Tables", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "VideoUrl", + table: "MenuItems", + type: "text", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ImageUrl", + table: "Tables"); + + migrationBuilder.DropColumn( + name: "VideoUrl", + table: "Tables"); + + migrationBuilder.DropColumn( + name: "VideoUrl", + table: "MenuItems"); + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260520135825_AddOrderReservationLink.Designer.cs b/src/Meezi.Infrastructure/Data/Migrations/20260520135825_AddOrderReservationLink.Designer.cs new file mode 100644 index 0000000..5406aac --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260520135825_AddOrderReservationLink.Designer.cs @@ -0,0 +1,1387 @@ +// +using System; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260520135825_AddOrderReservationLink")] + partial class AddOrderReservationLink + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ClockIn") + .HasColumnType("timestamp with time zone"); + + b.Property("ClockOut") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "Date") + .IsUnique(); + + b.ToTable("Attendances"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Branches"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("CoverImageUrl") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsVerified") + .HasColumnType("boolean"); + + b.Property("LogoUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("PlanExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("PreferredLanguage") + .IsRequired() + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SnappfoodVendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Cafes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AuthorName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("AuthorPhone") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Comment") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerRepliedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerReply") + .HasColumnType("text"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "CreatedAt"); + + b.ToTable("CafeReviews"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MaxDiscount") + .HasColumnType("numeric"); + + b.Property("MinOrderAmount") + .HasColumnType("numeric"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TargetGroup") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UsageLimit") + .HasColumnType("integer"); + + b.Property("UsedCount") + .HasColumnType("integer"); + + b.Property("Value") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Code") + .IsUnique(); + + b.ToTable("Coupons"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BirthDateJalali") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Group") + .HasColumnType("integer"); + + b.Property("LoyaltyPoints") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReferredBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("PinCode") + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Employees"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Deductions") + .HasColumnType("numeric"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsPaid") + .HasColumnType("boolean"); + + b.Property("MonthYear") + .IsRequired() + .HasColumnType("text"); + + b.Property("NetSalary") + .HasColumnType("numeric"); + + b.Property("OvertimePay") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "MonthYear") + .IsUnique(); + + b.ToTable("EmployeeSalaries"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("QuantityOnHand") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("ReorderLevel") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("Unit") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Ingredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndDate") + .HasColumnType("date"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("ReviewedBy") + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId"); + + b.ToTable("LeaveRequests"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("TaxId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("TaxId"); + + b.ToTable("MenuCategories"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CategoryId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("CategoryId"); + + b.ToTable("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CouponId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("EmployeeId") + .HasColumnType("text"); + + b.Property("OrderType") + .HasColumnType("integer"); + + b.Property("ReservationId") + .HasColumnType("text"); + + b.Property("SnappfoodOrderId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subtotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("TaxTotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Total") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId"); + + b.HasIndex("CouponId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("ReservationId"); + + b.HasIndex("TableId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UnitPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Reference") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DayOfWeek") + .HasColumnType("integer"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShiftType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "DayOfWeek") + .IsUnique(); + + b.ToTable("Shifts"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Delta") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("IngredientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Note") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("IngredientId"); + + b.ToTable("StockMovements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AmountRials") + .HasColumnType("bigint"); + + b.Property("AmountToman") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Authority") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Months") + .HasColumnType("integer"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("RefId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Authority"); + + b.HasIndex("CafeId"); + + b.ToTable("SubscriptionPayments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Floor") + .HasColumnType("text"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Number") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("QrCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId"); + + b.HasIndex("QrCode") + .IsUnique(); + + b.ToTable("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GuestName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GuestPhone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("PartySize") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("time without time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("TableId"); + + b.HasIndex("CafeId", "Date", "Time"); + + b.ToTable("TableReservations"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsCompound") + .HasColumnType("boolean"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("IsRequired") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Attendances") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Branches") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Reviews") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Coupons") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Customers") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Employees") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Salaries") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Ingredients") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("LeaveRequests") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuCategories") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Tax", "Tax") + .WithMany("MenuCategories") + .HasForeignKey("TaxId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Tax"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuItems") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuCategory", "Category") + .WithMany("MenuItems") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Orders") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Orders") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Coupon", "Coupon") + .WithMany("Orders") + .HasForeignKey("CouponId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Orders") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.TableReservation", "Reservation") + .WithMany() + .HasForeignKey("ReservationId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany("Orders") + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Coupon"); + + b.Navigation("Customer"); + + b.Navigation("Employee"); + + b.Navigation("Reservation"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("OrderItems") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Items") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MenuItem"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Payments") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Shifts") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Ingredient", "Ingredient") + .WithMany("Movements") + .HasForeignKey("IngredientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Ingredient"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("SubscriptionPayments") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Tables") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Tables") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany() + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Customer"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Taxes") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Navigation("Orders"); + + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Navigation("Branches"); + + b.Navigation("Coupons"); + + b.Navigation("Customers"); + + b.Navigation("Employees"); + + b.Navigation("Ingredients"); + + b.Navigation("MenuCategories"); + + b.Navigation("MenuItems"); + + b.Navigation("Orders"); + + b.Navigation("Reviews"); + + b.Navigation("SubscriptionPayments"); + + b.Navigation("Tables"); + + b.Navigation("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Navigation("Attendances"); + + b.Navigation("LeaveRequests"); + + b.Navigation("Orders"); + + b.Navigation("Salaries"); + + b.Navigation("Shifts"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Navigation("Movements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Navigation("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Navigation("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Navigation("Items"); + + b.Navigation("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Navigation("MenuCategories"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260520135825_AddOrderReservationLink.cs b/src/Meezi.Infrastructure/Data/Migrations/20260520135825_AddOrderReservationLink.cs new file mode 100644 index 0000000..ad32fe4 --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260520135825_AddOrderReservationLink.cs @@ -0,0 +1,49 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + /// + public partial class AddOrderReservationLink : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ReservationId", + table: "Orders", + type: "text", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_Orders_ReservationId", + table: "Orders", + column: "ReservationId"); + + migrationBuilder.AddForeignKey( + name: "FK_Orders_TableReservations_ReservationId", + table: "Orders", + column: "ReservationId", + principalTable: "TableReservations", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Orders_TableReservations_ReservationId", + table: "Orders"); + + migrationBuilder.DropIndex( + name: "IX_Orders_ReservationId", + table: "Orders"); + + migrationBuilder.DropColumn( + name: "ReservationId", + table: "Orders"); + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260520140806_AddOrderGuestName.Designer.cs b/src/Meezi.Infrastructure/Data/Migrations/20260520140806_AddOrderGuestName.Designer.cs new file mode 100644 index 0000000..108afbe --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260520140806_AddOrderGuestName.Designer.cs @@ -0,0 +1,1390 @@ +// +using System; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260520140806_AddOrderGuestName")] + partial class AddOrderGuestName + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ClockIn") + .HasColumnType("timestamp with time zone"); + + b.Property("ClockOut") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "Date") + .IsUnique(); + + b.ToTable("Attendances"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Branches"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("CoverImageUrl") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsVerified") + .HasColumnType("boolean"); + + b.Property("LogoUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("PlanExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("PreferredLanguage") + .IsRequired() + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SnappfoodVendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Cafes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AuthorName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("AuthorPhone") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Comment") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerRepliedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerReply") + .HasColumnType("text"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "CreatedAt"); + + b.ToTable("CafeReviews"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MaxDiscount") + .HasColumnType("numeric"); + + b.Property("MinOrderAmount") + .HasColumnType("numeric"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TargetGroup") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UsageLimit") + .HasColumnType("integer"); + + b.Property("UsedCount") + .HasColumnType("integer"); + + b.Property("Value") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Code") + .IsUnique(); + + b.ToTable("Coupons"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BirthDateJalali") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Group") + .HasColumnType("integer"); + + b.Property("LoyaltyPoints") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReferredBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("PinCode") + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Employees"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Deductions") + .HasColumnType("numeric"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsPaid") + .HasColumnType("boolean"); + + b.Property("MonthYear") + .IsRequired() + .HasColumnType("text"); + + b.Property("NetSalary") + .HasColumnType("numeric"); + + b.Property("OvertimePay") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "MonthYear") + .IsUnique(); + + b.ToTable("EmployeeSalaries"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("QuantityOnHand") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("ReorderLevel") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("Unit") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Ingredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndDate") + .HasColumnType("date"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("ReviewedBy") + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId"); + + b.ToTable("LeaveRequests"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("TaxId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("TaxId"); + + b.ToTable("MenuCategories"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CategoryId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("CategoryId"); + + b.ToTable("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CouponId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("EmployeeId") + .HasColumnType("text"); + + b.Property("GuestName") + .HasColumnType("text"); + + b.Property("OrderType") + .HasColumnType("integer"); + + b.Property("ReservationId") + .HasColumnType("text"); + + b.Property("SnappfoodOrderId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subtotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("TaxTotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Total") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId"); + + b.HasIndex("CouponId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("ReservationId"); + + b.HasIndex("TableId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UnitPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Reference") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DayOfWeek") + .HasColumnType("integer"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShiftType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "DayOfWeek") + .IsUnique(); + + b.ToTable("Shifts"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Delta") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("IngredientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Note") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("IngredientId"); + + b.ToTable("StockMovements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AmountRials") + .HasColumnType("bigint"); + + b.Property("AmountToman") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Authority") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Months") + .HasColumnType("integer"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("RefId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Authority"); + + b.HasIndex("CafeId"); + + b.ToTable("SubscriptionPayments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Floor") + .HasColumnType("text"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Number") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("QrCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId"); + + b.HasIndex("QrCode") + .IsUnique(); + + b.ToTable("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GuestName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GuestPhone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("PartySize") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("time without time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("TableId"); + + b.HasIndex("CafeId", "Date", "Time"); + + b.ToTable("TableReservations"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsCompound") + .HasColumnType("boolean"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("IsRequired") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Attendances") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Branches") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Reviews") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Coupons") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Customers") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Employees") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Salaries") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Ingredients") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("LeaveRequests") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuCategories") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Tax", "Tax") + .WithMany("MenuCategories") + .HasForeignKey("TaxId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Tax"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuItems") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuCategory", "Category") + .WithMany("MenuItems") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Orders") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Orders") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Coupon", "Coupon") + .WithMany("Orders") + .HasForeignKey("CouponId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Orders") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.TableReservation", "Reservation") + .WithMany() + .HasForeignKey("ReservationId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany("Orders") + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Coupon"); + + b.Navigation("Customer"); + + b.Navigation("Employee"); + + b.Navigation("Reservation"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("OrderItems") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Items") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MenuItem"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Payments") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Shifts") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Ingredient", "Ingredient") + .WithMany("Movements") + .HasForeignKey("IngredientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Ingredient"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("SubscriptionPayments") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Tables") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Tables") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany() + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Customer"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Taxes") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Navigation("Orders"); + + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Navigation("Branches"); + + b.Navigation("Coupons"); + + b.Navigation("Customers"); + + b.Navigation("Employees"); + + b.Navigation("Ingredients"); + + b.Navigation("MenuCategories"); + + b.Navigation("MenuItems"); + + b.Navigation("Orders"); + + b.Navigation("Reviews"); + + b.Navigation("SubscriptionPayments"); + + b.Navigation("Tables"); + + b.Navigation("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Navigation("Attendances"); + + b.Navigation("LeaveRequests"); + + b.Navigation("Orders"); + + b.Navigation("Salaries"); + + b.Navigation("Shifts"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Navigation("Movements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Navigation("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Navigation("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Navigation("Items"); + + b.Navigation("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Navigation("MenuCategories"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260520140806_AddOrderGuestName.cs b/src/Meezi.Infrastructure/Data/Migrations/20260520140806_AddOrderGuestName.cs new file mode 100644 index 0000000..44b5e76 --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260520140806_AddOrderGuestName.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + /// + public partial class AddOrderGuestName : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "GuestName", + table: "Orders", + type: "text", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "GuestName", + table: "Orders"); + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260520165836_PosTableSessionFields.Designer.cs b/src/Meezi.Infrastructure/Data/Migrations/20260520165836_PosTableSessionFields.Designer.cs new file mode 100644 index 0000000..8d1e77c --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260520165836_PosTableSessionFields.Designer.cs @@ -0,0 +1,1396 @@ +// +using System; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260520165836_PosTableSessionFields")] + partial class PosTableSessionFields + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ClockIn") + .HasColumnType("timestamp with time zone"); + + b.Property("ClockOut") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "Date") + .IsUnique(); + + b.ToTable("Attendances"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Branches"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("CoverImageUrl") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsVerified") + .HasColumnType("boolean"); + + b.Property("LogoUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("PlanExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("PreferredLanguage") + .IsRequired() + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SnappfoodVendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Cafes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AuthorName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("AuthorPhone") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Comment") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerRepliedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerReply") + .HasColumnType("text"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "CreatedAt"); + + b.ToTable("CafeReviews"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MaxDiscount") + .HasColumnType("numeric"); + + b.Property("MinOrderAmount") + .HasColumnType("numeric"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TargetGroup") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UsageLimit") + .HasColumnType("integer"); + + b.Property("UsedCount") + .HasColumnType("integer"); + + b.Property("Value") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Code") + .IsUnique(); + + b.ToTable("Coupons"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BirthDateJalali") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Group") + .HasColumnType("integer"); + + b.Property("LoyaltyPoints") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReferredBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("PinCode") + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Employees"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Deductions") + .HasColumnType("numeric"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsPaid") + .HasColumnType("boolean"); + + b.Property("MonthYear") + .IsRequired() + .HasColumnType("text"); + + b.Property("NetSalary") + .HasColumnType("numeric"); + + b.Property("OvertimePay") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "MonthYear") + .IsUnique(); + + b.ToTable("EmployeeSalaries"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("QuantityOnHand") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("ReorderLevel") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("Unit") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Ingredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndDate") + .HasColumnType("date"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("ReviewedBy") + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId"); + + b.ToTable("LeaveRequests"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("TaxId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("TaxId"); + + b.ToTable("MenuCategories"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CategoryId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("CategoryId"); + + b.ToTable("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CouponId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("EmployeeId") + .HasColumnType("text"); + + b.Property("GuestName") + .HasColumnType("text"); + + b.Property("GuestPhone") + .HasColumnType("text"); + + b.Property("OrderType") + .HasColumnType("integer"); + + b.Property("ReservationId") + .HasColumnType("text"); + + b.Property("SnappfoodOrderId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subtotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("TaxTotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Total") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId"); + + b.HasIndex("CouponId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("ReservationId"); + + b.HasIndex("TableId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UnitPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Reference") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DayOfWeek") + .HasColumnType("integer"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShiftType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "DayOfWeek") + .IsUnique(); + + b.ToTable("Shifts"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Delta") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("IngredientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Note") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("IngredientId"); + + b.ToTable("StockMovements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AmountRials") + .HasColumnType("bigint"); + + b.Property("AmountToman") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Authority") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Months") + .HasColumnType("integer"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("RefId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Authority"); + + b.HasIndex("CafeId"); + + b.ToTable("SubscriptionPayments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Floor") + .HasColumnType("text"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsCleaning") + .HasColumnType("boolean"); + + b.Property("Number") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("QrCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId"); + + b.HasIndex("QrCode") + .IsUnique(); + + b.ToTable("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GuestName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GuestPhone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("PartySize") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("time without time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("TableId"); + + b.HasIndex("CafeId", "Date", "Time"); + + b.ToTable("TableReservations"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsCompound") + .HasColumnType("boolean"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("IsRequired") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Attendances") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Branches") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Reviews") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Coupons") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Customers") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Employees") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Salaries") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Ingredients") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("LeaveRequests") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuCategories") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Tax", "Tax") + .WithMany("MenuCategories") + .HasForeignKey("TaxId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Tax"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuItems") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuCategory", "Category") + .WithMany("MenuItems") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Orders") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Orders") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Coupon", "Coupon") + .WithMany("Orders") + .HasForeignKey("CouponId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Orders") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.TableReservation", "Reservation") + .WithMany() + .HasForeignKey("ReservationId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany("Orders") + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Coupon"); + + b.Navigation("Customer"); + + b.Navigation("Employee"); + + b.Navigation("Reservation"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("OrderItems") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Items") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MenuItem"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Payments") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Shifts") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Ingredient", "Ingredient") + .WithMany("Movements") + .HasForeignKey("IngredientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Ingredient"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("SubscriptionPayments") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Tables") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Tables") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany() + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Customer"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Taxes") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Navigation("Orders"); + + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Navigation("Branches"); + + b.Navigation("Coupons"); + + b.Navigation("Customers"); + + b.Navigation("Employees"); + + b.Navigation("Ingredients"); + + b.Navigation("MenuCategories"); + + b.Navigation("MenuItems"); + + b.Navigation("Orders"); + + b.Navigation("Reviews"); + + b.Navigation("SubscriptionPayments"); + + b.Navigation("Tables"); + + b.Navigation("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Navigation("Attendances"); + + b.Navigation("LeaveRequests"); + + b.Navigation("Orders"); + + b.Navigation("Salaries"); + + b.Navigation("Shifts"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Navigation("Movements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Navigation("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Navigation("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Navigation("Items"); + + b.Navigation("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Navigation("MenuCategories"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260520165836_PosTableSessionFields.cs b/src/Meezi.Infrastructure/Data/Migrations/20260520165836_PosTableSessionFields.cs new file mode 100644 index 0000000..5ffa556 --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260520165836_PosTableSessionFields.cs @@ -0,0 +1,39 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + /// + public partial class PosTableSessionFields : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "IsCleaning", + table: "Tables", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "GuestPhone", + table: "Orders", + type: "text", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "IsCleaning", + table: "Tables"); + + migrationBuilder.DropColumn( + name: "GuestPhone", + table: "Orders"); + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260521134834_VoidOrderLine.Designer.cs b/src/Meezi.Infrastructure/Data/Migrations/20260521134834_VoidOrderLine.Designer.cs new file mode 100644 index 0000000..d68d6a5 --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260521134834_VoidOrderLine.Designer.cs @@ -0,0 +1,1405 @@ +// +using System; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260521134834_VoidOrderLine")] + partial class VoidOrderLine + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ClockIn") + .HasColumnType("timestamp with time zone"); + + b.Property("ClockOut") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "Date") + .IsUnique(); + + b.ToTable("Attendances"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Branches"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("CoverImageUrl") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsVerified") + .HasColumnType("boolean"); + + b.Property("LogoUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("PlanExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("PreferredLanguage") + .IsRequired() + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SnappfoodVendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Cafes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AuthorName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("AuthorPhone") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Comment") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerRepliedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerReply") + .HasColumnType("text"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "CreatedAt"); + + b.ToTable("CafeReviews"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MaxDiscount") + .HasColumnType("numeric"); + + b.Property("MinOrderAmount") + .HasColumnType("numeric"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TargetGroup") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UsageLimit") + .HasColumnType("integer"); + + b.Property("UsedCount") + .HasColumnType("integer"); + + b.Property("Value") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Code") + .IsUnique(); + + b.ToTable("Coupons"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BirthDateJalali") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Group") + .HasColumnType("integer"); + + b.Property("LoyaltyPoints") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReferredBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("PinCode") + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Employees"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Deductions") + .HasColumnType("numeric"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsPaid") + .HasColumnType("boolean"); + + b.Property("MonthYear") + .IsRequired() + .HasColumnType("text"); + + b.Property("NetSalary") + .HasColumnType("numeric"); + + b.Property("OvertimePay") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "MonthYear") + .IsUnique(); + + b.ToTable("EmployeeSalaries"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("QuantityOnHand") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("ReorderLevel") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("Unit") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Ingredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndDate") + .HasColumnType("date"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("ReviewedBy") + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId"); + + b.ToTable("LeaveRequests"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("TaxId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("TaxId"); + + b.ToTable("MenuCategories"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CategoryId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("CategoryId"); + + b.ToTable("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CouponId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("EmployeeId") + .HasColumnType("text"); + + b.Property("GuestName") + .HasColumnType("text"); + + b.Property("GuestPhone") + .HasColumnType("text"); + + b.Property("OrderType") + .HasColumnType("integer"); + + b.Property("ReservationId") + .HasColumnType("text"); + + b.Property("SnappfoodOrderId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subtotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("TaxTotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Total") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId"); + + b.HasIndex("CouponId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("ReservationId"); + + b.HasIndex("TableId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsVoided") + .HasColumnType("boolean"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UnitPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VoidedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VoidedByUserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Reference") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DayOfWeek") + .HasColumnType("integer"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShiftType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "DayOfWeek") + .IsUnique(); + + b.ToTable("Shifts"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Delta") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("IngredientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Note") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("IngredientId"); + + b.ToTable("StockMovements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AmountRials") + .HasColumnType("bigint"); + + b.Property("AmountToman") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Authority") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Months") + .HasColumnType("integer"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("RefId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Authority"); + + b.HasIndex("CafeId"); + + b.ToTable("SubscriptionPayments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Floor") + .HasColumnType("text"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsCleaning") + .HasColumnType("boolean"); + + b.Property("Number") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("QrCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId"); + + b.HasIndex("QrCode") + .IsUnique(); + + b.ToTable("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GuestName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GuestPhone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("PartySize") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("time without time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("TableId"); + + b.HasIndex("CafeId", "Date", "Time"); + + b.ToTable("TableReservations"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsCompound") + .HasColumnType("boolean"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("IsRequired") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Attendances") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Branches") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Reviews") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Coupons") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Customers") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Employees") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Salaries") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Ingredients") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("LeaveRequests") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuCategories") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Tax", "Tax") + .WithMany("MenuCategories") + .HasForeignKey("TaxId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Tax"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuItems") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuCategory", "Category") + .WithMany("MenuItems") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Orders") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Orders") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Coupon", "Coupon") + .WithMany("Orders") + .HasForeignKey("CouponId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Orders") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.TableReservation", "Reservation") + .WithMany() + .HasForeignKey("ReservationId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany("Orders") + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Coupon"); + + b.Navigation("Customer"); + + b.Navigation("Employee"); + + b.Navigation("Reservation"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("OrderItems") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Items") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MenuItem"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Payments") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Shifts") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Ingredient", "Ingredient") + .WithMany("Movements") + .HasForeignKey("IngredientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Ingredient"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("SubscriptionPayments") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Tables") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Tables") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany() + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Customer"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Taxes") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Navigation("Orders"); + + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Navigation("Branches"); + + b.Navigation("Coupons"); + + b.Navigation("Customers"); + + b.Navigation("Employees"); + + b.Navigation("Ingredients"); + + b.Navigation("MenuCategories"); + + b.Navigation("MenuItems"); + + b.Navigation("Orders"); + + b.Navigation("Reviews"); + + b.Navigation("SubscriptionPayments"); + + b.Navigation("Tables"); + + b.Navigation("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Navigation("Attendances"); + + b.Navigation("LeaveRequests"); + + b.Navigation("Orders"); + + b.Navigation("Salaries"); + + b.Navigation("Shifts"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Navigation("Movements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Navigation("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Navigation("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Navigation("Items"); + + b.Navigation("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Navigation("MenuCategories"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260521134834_VoidOrderLine.cs b/src/Meezi.Infrastructure/Data/Migrations/20260521134834_VoidOrderLine.cs new file mode 100644 index 0000000..8f1ce0c --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260521134834_VoidOrderLine.cs @@ -0,0 +1,41 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + /// + public partial class VoidOrderLine : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "IsVoided", + table: "OrderItems", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "VoidedAt", + table: "OrderItems", + type: "timestamp with time zone", + nullable: true); + + migrationBuilder.AddColumn( + name: "VoidedByUserId", + table: "OrderItems", + type: "text", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn(name: "VoidedByUserId", table: "OrderItems"); + migrationBuilder.DropColumn(name: "VoidedAt", table: "OrderItems"); + migrationBuilder.DropColumn(name: "IsVoided", table: "OrderItems"); + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260521182819_EmployeeBranchId.Designer.cs b/src/Meezi.Infrastructure/Data/Migrations/20260521182819_EmployeeBranchId.Designer.cs new file mode 100644 index 0000000..788aa62 --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260521182819_EmployeeBranchId.Designer.cs @@ -0,0 +1,1415 @@ +// +using System; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260521182819_EmployeeBranchId")] + partial class EmployeeBranchId + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ClockIn") + .HasColumnType("timestamp with time zone"); + + b.Property("ClockOut") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "Date") + .IsUnique(); + + b.ToTable("Attendances"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Branches"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("CoverImageUrl") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsVerified") + .HasColumnType("boolean"); + + b.Property("LogoUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("PlanExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("PreferredLanguage") + .IsRequired() + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SnappfoodVendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Cafes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AuthorName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("AuthorPhone") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Comment") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerRepliedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerReply") + .HasColumnType("text"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "CreatedAt"); + + b.ToTable("CafeReviews"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MaxDiscount") + .HasColumnType("numeric"); + + b.Property("MinOrderAmount") + .HasColumnType("numeric"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TargetGroup") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UsageLimit") + .HasColumnType("integer"); + + b.Property("UsedCount") + .HasColumnType("integer"); + + b.Property("Value") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Code") + .IsUnique(); + + b.ToTable("Coupons"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BirthDateJalali") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Group") + .HasColumnType("integer"); + + b.Property("LoyaltyPoints") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReferredBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("PinCode") + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Employees"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Deductions") + .HasColumnType("numeric"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsPaid") + .HasColumnType("boolean"); + + b.Property("MonthYear") + .IsRequired() + .HasColumnType("text"); + + b.Property("NetSalary") + .HasColumnType("numeric"); + + b.Property("OvertimePay") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "MonthYear") + .IsUnique(); + + b.ToTable("EmployeeSalaries"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("QuantityOnHand") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("ReorderLevel") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("Unit") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Ingredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndDate") + .HasColumnType("date"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("ReviewedBy") + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId"); + + b.ToTable("LeaveRequests"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("TaxId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("TaxId"); + + b.ToTable("MenuCategories"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CategoryId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("CategoryId"); + + b.ToTable("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CouponId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("EmployeeId") + .HasColumnType("text"); + + b.Property("GuestName") + .HasColumnType("text"); + + b.Property("GuestPhone") + .HasColumnType("text"); + + b.Property("OrderType") + .HasColumnType("integer"); + + b.Property("ReservationId") + .HasColumnType("text"); + + b.Property("SnappfoodOrderId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subtotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("TaxTotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Total") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId"); + + b.HasIndex("CouponId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("ReservationId"); + + b.HasIndex("TableId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsVoided") + .HasColumnType("boolean"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UnitPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VoidedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VoidedByUserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Reference") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DayOfWeek") + .HasColumnType("integer"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShiftType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "DayOfWeek") + .IsUnique(); + + b.ToTable("Shifts"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Delta") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("IngredientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Note") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("IngredientId"); + + b.ToTable("StockMovements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AmountRials") + .HasColumnType("bigint"); + + b.Property("AmountToman") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Authority") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Months") + .HasColumnType("integer"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("RefId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Authority"); + + b.HasIndex("CafeId"); + + b.ToTable("SubscriptionPayments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Floor") + .HasColumnType("text"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsCleaning") + .HasColumnType("boolean"); + + b.Property("Number") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("QrCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId"); + + b.HasIndex("QrCode") + .IsUnique(); + + b.ToTable("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GuestName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GuestPhone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("PartySize") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("time without time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("TableId"); + + b.HasIndex("CafeId", "Date", "Time"); + + b.ToTable("TableReservations"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsCompound") + .HasColumnType("boolean"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("IsRequired") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Attendances") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Branches") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Reviews") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Coupons") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Customers") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.HasOne("Meezi.Core.Entities.Branch", null) + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Employees") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Salaries") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Ingredients") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("LeaveRequests") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuCategories") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Tax", "Tax") + .WithMany("MenuCategories") + .HasForeignKey("TaxId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Tax"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuItems") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuCategory", "Category") + .WithMany("MenuItems") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Orders") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Orders") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Coupon", "Coupon") + .WithMany("Orders") + .HasForeignKey("CouponId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Orders") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.TableReservation", "Reservation") + .WithMany() + .HasForeignKey("ReservationId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany("Orders") + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Coupon"); + + b.Navigation("Customer"); + + b.Navigation("Employee"); + + b.Navigation("Reservation"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("OrderItems") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Items") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MenuItem"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Payments") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Shifts") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Ingredient", "Ingredient") + .WithMany("Movements") + .HasForeignKey("IngredientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Ingredient"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("SubscriptionPayments") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Tables") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Tables") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany() + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Customer"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Taxes") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Navigation("Orders"); + + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Navigation("Branches"); + + b.Navigation("Coupons"); + + b.Navigation("Customers"); + + b.Navigation("Employees"); + + b.Navigation("Ingredients"); + + b.Navigation("MenuCategories"); + + b.Navigation("MenuItems"); + + b.Navigation("Orders"); + + b.Navigation("Reviews"); + + b.Navigation("SubscriptionPayments"); + + b.Navigation("Tables"); + + b.Navigation("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Navigation("Attendances"); + + b.Navigation("LeaveRequests"); + + b.Navigation("Orders"); + + b.Navigation("Salaries"); + + b.Navigation("Shifts"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Navigation("Movements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Navigation("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Navigation("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Navigation("Items"); + + b.Navigation("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Navigation("MenuCategories"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260521182819_EmployeeBranchId.cs b/src/Meezi.Infrastructure/Data/Migrations/20260521182819_EmployeeBranchId.cs new file mode 100644 index 0000000..309702f --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260521182819_EmployeeBranchId.cs @@ -0,0 +1,49 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + /// + public partial class EmployeeBranchId : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "BranchId", + table: "Employees", + type: "text", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_Employees_BranchId", + table: "Employees", + column: "BranchId"); + + migrationBuilder.AddForeignKey( + name: "FK_Employees_Branches_BranchId", + table: "Employees", + column: "BranchId", + principalTable: "Branches", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Employees_Branches_BranchId", + table: "Employees"); + + migrationBuilder.DropIndex( + name: "IX_Employees_BranchId", + table: "Employees"); + + migrationBuilder.DropColumn( + name: "BranchId", + table: "Employees"); + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260521184559_DailyQueueTickets.Designer.cs b/src/Meezi.Infrastructure/Data/Migrations/20260521184559_DailyQueueTickets.Designer.cs new file mode 100644 index 0000000..164a871 --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260521184559_DailyQueueTickets.Designer.cs @@ -0,0 +1,1492 @@ +// +using System; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260521184559_DailyQueueTickets")] + partial class DailyQueueTickets + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ClockIn") + .HasColumnType("timestamp with time zone"); + + b.Property("ClockOut") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "Date") + .IsUnique(); + + b.ToTable("Attendances"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Branches"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("CoverImageUrl") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsVerified") + .HasColumnType("boolean"); + + b.Property("LogoUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("PlanExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("PreferredLanguage") + .IsRequired() + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SnappfoodVendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Cafes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AuthorName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("AuthorPhone") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Comment") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerRepliedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerReply") + .HasColumnType("text"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "CreatedAt"); + + b.ToTable("CafeReviews"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MaxDiscount") + .HasColumnType("numeric"); + + b.Property("MinOrderAmount") + .HasColumnType("numeric"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TargetGroup") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UsageLimit") + .HasColumnType("integer"); + + b.Property("UsedCount") + .HasColumnType("integer"); + + b.Property("Value") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Code") + .IsUnique(); + + b.ToTable("Coupons"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BirthDateJalali") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Group") + .HasColumnType("integer"); + + b.Property("LoyaltyPoints") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReferredBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("PinCode") + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Employees"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Deductions") + .HasColumnType("numeric"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsPaid") + .HasColumnType("boolean"); + + b.Property("MonthYear") + .IsRequired() + .HasColumnType("text"); + + b.Property("NetSalary") + .HasColumnType("numeric"); + + b.Property("OvertimePay") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "MonthYear") + .IsUnique(); + + b.ToTable("EmployeeSalaries"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("QuantityOnHand") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("ReorderLevel") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("Unit") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Ingredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndDate") + .HasColumnType("date"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("ReviewedBy") + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId"); + + b.ToTable("LeaveRequests"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("TaxId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("TaxId"); + + b.ToTable("MenuCategories"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CategoryId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("CategoryId"); + + b.ToTable("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CouponId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("EmployeeId") + .HasColumnType("text"); + + b.Property("GuestName") + .HasColumnType("text"); + + b.Property("GuestPhone") + .HasColumnType("text"); + + b.Property("OrderType") + .HasColumnType("integer"); + + b.Property("ReservationId") + .HasColumnType("text"); + + b.Property("SnappfoodOrderId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subtotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("TaxTotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Total") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId"); + + b.HasIndex("CouponId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("ReservationId"); + + b.HasIndex("TableId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsVoided") + .HasColumnType("boolean"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UnitPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VoidedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VoidedByUserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Reference") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.QueueTicket", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerLabel") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedByUserId") + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("integer"); + + b.Property("OrderId") + .HasColumnType("text"); + + b.Property("ServiceDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("OrderId"); + + b.HasIndex("CafeId", "BranchId", "ServiceDate", "Number") + .IsUnique(); + + b.ToTable("QueueTickets"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DayOfWeek") + .HasColumnType("integer"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShiftType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "DayOfWeek") + .IsUnique(); + + b.ToTable("Shifts"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Delta") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("IngredientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Note") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("IngredientId"); + + b.ToTable("StockMovements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AmountRials") + .HasColumnType("bigint"); + + b.Property("AmountToman") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Authority") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Months") + .HasColumnType("integer"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("RefId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Authority"); + + b.HasIndex("CafeId"); + + b.ToTable("SubscriptionPayments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Floor") + .HasColumnType("text"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsCleaning") + .HasColumnType("boolean"); + + b.Property("Number") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("QrCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId"); + + b.HasIndex("QrCode") + .IsUnique(); + + b.ToTable("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GuestName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GuestPhone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("PartySize") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("time without time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("TableId"); + + b.HasIndex("CafeId", "Date", "Time"); + + b.ToTable("TableReservations"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsCompound") + .HasColumnType("boolean"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("IsRequired") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Attendances") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Branches") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Reviews") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Coupons") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Customers") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.HasOne("Meezi.Core.Entities.Branch", null) + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Employees") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Salaries") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Ingredients") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("LeaveRequests") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuCategories") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Tax", "Tax") + .WithMany("MenuCategories") + .HasForeignKey("TaxId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Tax"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuItems") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuCategory", "Category") + .WithMany("MenuItems") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Orders") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Orders") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Coupon", "Coupon") + .WithMany("Orders") + .HasForeignKey("CouponId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Orders") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.TableReservation", "Reservation") + .WithMany() + .HasForeignKey("ReservationId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany("Orders") + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Coupon"); + + b.Navigation("Customer"); + + b.Navigation("Employee"); + + b.Navigation("Reservation"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("OrderItems") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Items") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MenuItem"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Payments") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.QueueTicket", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Shifts") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Ingredient", "Ingredient") + .WithMany("Movements") + .HasForeignKey("IngredientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Ingredient"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("SubscriptionPayments") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Tables") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Tables") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany() + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Customer"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Taxes") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Navigation("Orders"); + + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Navigation("Branches"); + + b.Navigation("Coupons"); + + b.Navigation("Customers"); + + b.Navigation("Employees"); + + b.Navigation("Ingredients"); + + b.Navigation("MenuCategories"); + + b.Navigation("MenuItems"); + + b.Navigation("Orders"); + + b.Navigation("Reviews"); + + b.Navigation("SubscriptionPayments"); + + b.Navigation("Tables"); + + b.Navigation("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Navigation("Attendances"); + + b.Navigation("LeaveRequests"); + + b.Navigation("Orders"); + + b.Navigation("Salaries"); + + b.Navigation("Shifts"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Navigation("Movements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Navigation("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Navigation("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Navigation("Items"); + + b.Navigation("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Navigation("MenuCategories"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260521184559_DailyQueueTickets.cs b/src/Meezi.Infrastructure/Data/Migrations/20260521184559_DailyQueueTickets.cs new file mode 100644 index 0000000..670f864 --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260521184559_DailyQueueTickets.cs @@ -0,0 +1,78 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + /// + public partial class DailyQueueTickets : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "QueueTickets", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + BranchId = table.Column(type: "text", nullable: true), + ServiceDate = table.Column(type: "date", nullable: false), + Number = table.Column(type: "integer", nullable: false), + CustomerLabel = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + IssuedByUserId = table.Column(type: "text", nullable: true), + Status = table.Column(type: "integer", nullable: false), + OrderId = table.Column(type: "text", nullable: true), + IssuedAt = table.Column(type: "timestamp with time zone", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CafeId = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_QueueTickets", x => x.Id); + table.ForeignKey( + name: "FK_QueueTickets_Branches_BranchId", + column: x => x.BranchId, + principalTable: "Branches", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "FK_QueueTickets_Cafes_CafeId", + column: x => x.CafeId, + principalTable: "Cafes", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_QueueTickets_Orders_OrderId", + column: x => x.OrderId, + principalTable: "Orders", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateIndex( + name: "IX_QueueTickets_BranchId", + table: "QueueTickets", + column: "BranchId"); + + migrationBuilder.CreateIndex( + name: "IX_QueueTickets_CafeId_BranchId_ServiceDate_Number", + table: "QueueTickets", + columns: new[] { "CafeId", "BranchId", "ServiceDate", "Number" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_QueueTickets_OrderId", + table: "QueueTickets", + column: "OrderId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "QueueTickets"); + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260521203053_AddBranchEntity.Designer.cs b/src/Meezi.Infrastructure/Data/Migrations/20260521203053_AddBranchEntity.Designer.cs new file mode 100644 index 0000000..f5e6e36 --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260521203053_AddBranchEntity.Designer.cs @@ -0,0 +1,1509 @@ +// +using System; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260521203053_AddBranchEntity")] + partial class AddBranchEntity + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ClockIn") + .HasColumnType("timestamp with time zone"); + + b.Property("ClockOut") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "Date") + .IsUnique(); + + b.ToTable("Attendances"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("City") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Phone") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "IsActive"); + + b.ToTable("Branches"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("CoverImageUrl") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsVerified") + .HasColumnType("boolean"); + + b.Property("LogoUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("PlanExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("PreferredLanguage") + .IsRequired() + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SnappfoodVendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Cafes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AuthorName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("AuthorPhone") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Comment") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerRepliedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerReply") + .HasColumnType("text"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "CreatedAt"); + + b.ToTable("CafeReviews"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MaxDiscount") + .HasColumnType("numeric"); + + b.Property("MinOrderAmount") + .HasColumnType("numeric"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TargetGroup") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UsageLimit") + .HasColumnType("integer"); + + b.Property("UsedCount") + .HasColumnType("integer"); + + b.Property("Value") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Code") + .IsUnique(); + + b.ToTable("Coupons"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BirthDateJalali") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Group") + .HasColumnType("integer"); + + b.Property("LoyaltyPoints") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReferredBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("PinCode") + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Employees"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Deductions") + .HasColumnType("numeric"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsPaid") + .HasColumnType("boolean"); + + b.Property("MonthYear") + .IsRequired() + .HasColumnType("text"); + + b.Property("NetSalary") + .HasColumnType("numeric"); + + b.Property("OvertimePay") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "MonthYear") + .IsUnique(); + + b.ToTable("EmployeeSalaries"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("QuantityOnHand") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("ReorderLevel") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("Unit") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Ingredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndDate") + .HasColumnType("date"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("ReviewedBy") + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId"); + + b.ToTable("LeaveRequests"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("TaxId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("TaxId"); + + b.ToTable("MenuCategories"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CategoryId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("CategoryId"); + + b.ToTable("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CouponId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("EmployeeId") + .HasColumnType("text"); + + b.Property("GuestName") + .HasColumnType("text"); + + b.Property("GuestPhone") + .HasColumnType("text"); + + b.Property("OrderType") + .HasColumnType("integer"); + + b.Property("ReservationId") + .HasColumnType("text"); + + b.Property("SnappfoodOrderId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subtotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("TaxTotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Total") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId"); + + b.HasIndex("CouponId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("ReservationId"); + + b.HasIndex("TableId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsVoided") + .HasColumnType("boolean"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UnitPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VoidedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VoidedByUserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Reference") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.QueueTicket", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerLabel") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedByUserId") + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("integer"); + + b.Property("OrderId") + .HasColumnType("text"); + + b.Property("ServiceDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("OrderId"); + + b.HasIndex("CafeId", "BranchId", "ServiceDate", "Number") + .IsUnique(); + + b.ToTable("QueueTickets"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DayOfWeek") + .HasColumnType("integer"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShiftType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "DayOfWeek") + .IsUnique(); + + b.ToTable("Shifts"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Delta") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("IngredientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Note") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("IngredientId"); + + b.ToTable("StockMovements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AmountRials") + .HasColumnType("bigint"); + + b.Property("AmountToman") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Authority") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Months") + .HasColumnType("integer"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("RefId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Authority"); + + b.HasIndex("CafeId"); + + b.ToTable("SubscriptionPayments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Floor") + .HasColumnType("text"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsCleaning") + .HasColumnType("boolean"); + + b.Property("Number") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("QrCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId"); + + b.HasIndex("QrCode") + .IsUnique(); + + b.ToTable("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GuestName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GuestPhone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("PartySize") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("time without time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("TableId"); + + b.HasIndex("CafeId", "Date", "Time"); + + b.ToTable("TableReservations"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsCompound") + .HasColumnType("boolean"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("IsRequired") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Attendances") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Branches") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Reviews") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Coupons") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Customers") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Staff") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Employees") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Salaries") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Ingredients") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("LeaveRequests") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuCategories") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Tax", "Tax") + .WithMany("MenuCategories") + .HasForeignKey("TaxId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Tax"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuItems") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuCategory", "Category") + .WithMany("MenuItems") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Orders") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Orders") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Coupon", "Coupon") + .WithMany("Orders") + .HasForeignKey("CouponId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Orders") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.TableReservation", "Reservation") + .WithMany() + .HasForeignKey("ReservationId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany("Orders") + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Coupon"); + + b.Navigation("Customer"); + + b.Navigation("Employee"); + + b.Navigation("Reservation"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("OrderItems") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Items") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MenuItem"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Payments") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.QueueTicket", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Shifts") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Ingredient", "Ingredient") + .WithMany("Movements") + .HasForeignKey("IngredientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Ingredient"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("SubscriptionPayments") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Tables") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Tables") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany() + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Customer"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Taxes") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Navigation("Orders"); + + b.Navigation("Staff"); + + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Navigation("Branches"); + + b.Navigation("Coupons"); + + b.Navigation("Customers"); + + b.Navigation("Employees"); + + b.Navigation("Ingredients"); + + b.Navigation("MenuCategories"); + + b.Navigation("MenuItems"); + + b.Navigation("Orders"); + + b.Navigation("Reviews"); + + b.Navigation("SubscriptionPayments"); + + b.Navigation("Tables"); + + b.Navigation("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Navigation("Attendances"); + + b.Navigation("LeaveRequests"); + + b.Navigation("Orders"); + + b.Navigation("Salaries"); + + b.Navigation("Shifts"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Navigation("Movements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Navigation("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Navigation("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Navigation("Items"); + + b.Navigation("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Navigation("MenuCategories"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260521203053_AddBranchEntity.cs b/src/Meezi.Infrastructure/Data/Migrations/20260521203053_AddBranchEntity.cs new file mode 100644 index 0000000..fc247e2 --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260521203053_AddBranchEntity.cs @@ -0,0 +1,128 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + /// + public partial class AddBranchEntity : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_Branches_CafeId", + table: "Branches"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "Branches", + type: "character varying(200)", + maxLength: 200, + nullable: false, + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AlterColumn( + name: "City", + table: "Branches", + type: "character varying(100)", + maxLength: 100, + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Address", + table: "Branches", + type: "character varying(500)", + maxLength: 500, + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AddColumn( + name: "IsActive", + table: "Branches", + type: "boolean", + nullable: false, + defaultValue: true); + + migrationBuilder.AddColumn( + name: "Phone", + table: "Branches", + type: "character varying(20)", + maxLength: 20, + nullable: true); + + migrationBuilder.AddColumn( + name: "UpdatedAt", + table: "Branches", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "NOW() AT TIME ZONE 'UTC'"); + + migrationBuilder.CreateIndex( + name: "IX_Branches_CafeId_IsActive", + table: "Branches", + columns: new[] { "CafeId", "IsActive" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_Branches_CafeId_IsActive", + table: "Branches"); + + migrationBuilder.DropColumn( + name: "IsActive", + table: "Branches"); + + migrationBuilder.DropColumn( + name: "Phone", + table: "Branches"); + + migrationBuilder.DropColumn( + name: "UpdatedAt", + table: "Branches"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "Branches", + type: "text", + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(200)", + oldMaxLength: 200); + + migrationBuilder.AlterColumn( + name: "City", + table: "Branches", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(100)", + oldMaxLength: 100, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Address", + table: "Branches", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(500)", + oldMaxLength: 500, + oldNullable: true); + + migrationBuilder.CreateIndex( + name: "IX_Branches_CafeId", + table: "Branches", + column: "CafeId"); + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260521205152_AddShiftAndCashTransaction.Designer.cs b/src/Meezi.Infrastructure/Data/Migrations/20260521205152_AddShiftAndCashTransaction.Designer.cs new file mode 100644 index 0000000..05498f2 --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260521205152_AddShiftAndCashTransaction.Designer.cs @@ -0,0 +1,1683 @@ +// +using System; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260521205152_AddShiftAndCashTransaction")] + partial class AddShiftAndCashTransaction + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ClockIn") + .HasColumnType("timestamp with time zone"); + + b.Property("ClockOut") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "Date") + .IsUnique(); + + b.ToTable("Attendances"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("City") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Phone") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "IsActive"); + + b.ToTable("Branches"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("CoverImageUrl") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsVerified") + .HasColumnType("boolean"); + + b.Property("LogoUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("PlanExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("PreferredLanguage") + .IsRequired() + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SnappfoodVendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Cafes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AuthorName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("AuthorPhone") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Comment") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerRepliedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerReply") + .HasColumnType("text"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "CreatedAt"); + + b.ToTable("CafeReviews"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CashTransaction", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("ReferenceId") + .HasColumnType("text"); + + b.Property("ShiftId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("ShiftId"); + + b.HasIndex("CafeId", "BranchId"); + + b.ToTable("CashTransactions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MaxDiscount") + .HasColumnType("numeric"); + + b.Property("MinOrderAmount") + .HasColumnType("numeric"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TargetGroup") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UsageLimit") + .HasColumnType("integer"); + + b.Property("UsedCount") + .HasColumnType("integer"); + + b.Property("Value") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Code") + .IsUnique(); + + b.ToTable("Coupons"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BirthDateJalali") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Group") + .HasColumnType("integer"); + + b.Property("LoyaltyPoints") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReferredBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("PinCode") + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Employees"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Deductions") + .HasColumnType("numeric"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsPaid") + .HasColumnType("boolean"); + + b.Property("MonthYear") + .IsRequired() + .HasColumnType("text"); + + b.Property("NetSalary") + .HasColumnType("numeric"); + + b.Property("OvertimePay") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "MonthYear") + .IsUnique(); + + b.ToTable("EmployeeSalaries"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSchedule", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DayOfWeek") + .HasColumnType("integer"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShiftType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "DayOfWeek") + .IsUnique(); + + b.ToTable("EmployeeSchedules", (string)null); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("QuantityOnHand") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("ReorderLevel") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("Unit") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Ingredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndDate") + .HasColumnType("date"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("ReviewedBy") + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId"); + + b.ToTable("LeaveRequests"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("TaxId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("TaxId"); + + b.ToTable("MenuCategories"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CategoryId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("CategoryId"); + + b.ToTable("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CouponId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("EmployeeId") + .HasColumnType("text"); + + b.Property("GuestName") + .HasColumnType("text"); + + b.Property("GuestPhone") + .HasColumnType("text"); + + b.Property("OrderType") + .HasColumnType("integer"); + + b.Property("ReservationId") + .HasColumnType("text"); + + b.Property("SnappfoodOrderId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subtotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("TaxTotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Total") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId"); + + b.HasIndex("CouponId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("ReservationId"); + + b.HasIndex("TableId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsVoided") + .HasColumnType("boolean"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UnitPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VoidedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VoidedByUserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Reference") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.QueueTicket", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerLabel") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedByUserId") + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("integer"); + + b.Property("OrderId") + .HasColumnType("text"); + + b.Property("ServiceDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("OrderId"); + + b.HasIndex("CafeId", "BranchId", "ServiceDate", "Number") + .IsUnique(); + + b.ToTable("QueueTickets"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClosedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ClosedByUserId") + .HasColumnType("text"); + + b.Property("ClosingCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Discrepancy") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ExpectedCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("OpenedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OpenedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("OpeningCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("ClosedByUserId"); + + b.HasIndex("OpenedByUserId"); + + b.HasIndex("BranchId", "Status"); + + b.ToTable("RegisterShifts", (string)null); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Delta") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("IngredientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Note") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("IngredientId"); + + b.ToTable("StockMovements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AmountRials") + .HasColumnType("bigint"); + + b.Property("AmountToman") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Authority") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Months") + .HasColumnType("integer"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("RefId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Authority"); + + b.HasIndex("CafeId"); + + b.ToTable("SubscriptionPayments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Floor") + .HasColumnType("text"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsCleaning") + .HasColumnType("boolean"); + + b.Property("Number") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("QrCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId"); + + b.HasIndex("QrCode") + .IsUnique(); + + b.ToTable("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GuestName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GuestPhone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("PartySize") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("time without time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("TableId"); + + b.HasIndex("CafeId", "Date", "Time"); + + b.ToTable("TableReservations"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsCompound") + .HasColumnType("boolean"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("IsRequired") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Attendances") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Branches") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Reviews") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CashTransaction", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Shift", "Shift") + .WithMany("Transactions") + .HasForeignKey("ShiftId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Shift"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Coupons") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Customers") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Staff") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Employees") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Salaries") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSchedule", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Schedules") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Ingredients") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("LeaveRequests") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuCategories") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Tax", "Tax") + .WithMany("MenuCategories") + .HasForeignKey("TaxId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Tax"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuItems") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuCategory", "Category") + .WithMany("MenuItems") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Orders") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Orders") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Coupon", "Coupon") + .WithMany("Orders") + .HasForeignKey("CouponId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Orders") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.TableReservation", "Reservation") + .WithMany() + .HasForeignKey("ReservationId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany("Orders") + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Coupon"); + + b.Navigation("Customer"); + + b.Navigation("Employee"); + + b.Navigation("Reservation"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("OrderItems") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Items") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MenuItem"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Payments") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.QueueTicket", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Employee", "ClosedBy") + .WithMany() + .HasForeignKey("ClosedByUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "OpenedBy") + .WithMany() + .HasForeignKey("OpenedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("ClosedBy"); + + b.Navigation("OpenedBy"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Ingredient", "Ingredient") + .WithMany("Movements") + .HasForeignKey("IngredientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Ingredient"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("SubscriptionPayments") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Tables") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Tables") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany() + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Customer"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Taxes") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Navigation("Orders"); + + b.Navigation("Staff"); + + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Navigation("Branches"); + + b.Navigation("Coupons"); + + b.Navigation("Customers"); + + b.Navigation("Employees"); + + b.Navigation("Ingredients"); + + b.Navigation("MenuCategories"); + + b.Navigation("MenuItems"); + + b.Navigation("Orders"); + + b.Navigation("Reviews"); + + b.Navigation("SubscriptionPayments"); + + b.Navigation("Tables"); + + b.Navigation("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Navigation("Attendances"); + + b.Navigation("LeaveRequests"); + + b.Navigation("Orders"); + + b.Navigation("Salaries"); + + b.Navigation("Schedules"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Navigation("Movements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Navigation("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Navigation("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Navigation("Items"); + + b.Navigation("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Navigation("MenuCategories"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260521205152_AddShiftAndCashTransaction.cs b/src/Meezi.Infrastructure/Data/Migrations/20260521205152_AddShiftAndCashTransaction.cs new file mode 100644 index 0000000..199a54d --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260521205152_AddShiftAndCashTransaction.cs @@ -0,0 +1,184 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + /// + public partial class AddShiftAndCashTransaction : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Shifts_Employees_EmployeeId", + table: "Shifts"); + + migrationBuilder.RenameTable( + name: "Shifts", + newName: "EmployeeSchedules"); + + migrationBuilder.RenameIndex( + name: "IX_Shifts_EmployeeId_DayOfWeek", + table: "EmployeeSchedules", + newName: "IX_EmployeeSchedules_EmployeeId_DayOfWeek"); + + migrationBuilder.AddForeignKey( + name: "FK_EmployeeSchedules_Employees_EmployeeId", + table: "EmployeeSchedules", + column: "EmployeeId", + principalTable: "Employees", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.CreateTable( + name: "RegisterShifts", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + CafeId = table.Column(type: "text", nullable: false), + BranchId = table.Column(type: "text", nullable: false), + OpenedByUserId = table.Column(type: "text", nullable: false), + ClosedByUserId = table.Column(type: "text", nullable: true), + OpenedAt = table.Column(type: "timestamp with time zone", nullable: false), + ClosedAt = table.Column(type: "timestamp with time zone", nullable: true), + OpeningCash = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + ClosingCash = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: true), + ExpectedCash = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + Discrepancy = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: true), + Status = table.Column(type: "integer", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_RegisterShifts", x => x.Id); + table.ForeignKey( + name: "FK_RegisterShifts_Branches_BranchId", + column: x => x.BranchId, + principalTable: "Branches", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_RegisterShifts_Cafes_CafeId", + column: x => x.CafeId, + principalTable: "Cafes", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_RegisterShifts_Employees_ClosedByUserId", + column: x => x.ClosedByUserId, + principalTable: "Employees", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "FK_RegisterShifts_Employees_OpenedByUserId", + column: x => x.OpenedByUserId, + principalTable: "Employees", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "CashTransactions", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + ShiftId = table.Column(type: "text", nullable: false), + BranchId = table.Column(type: "text", nullable: true), + Type = table.Column(type: "integer", nullable: false), + Method = table.Column(type: "integer", nullable: false), + Amount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + ReferenceId = table.Column(type: "text", nullable: true), + Note = table.Column(type: "text", nullable: true), + CreatedByUserId = table.Column(type: "text", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CafeId = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_CashTransactions", x => x.Id); + table.ForeignKey( + name: "FK_CashTransactions_Branches_BranchId", + column: x => x.BranchId, + principalTable: "Branches", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "FK_CashTransactions_RegisterShifts_ShiftId", + column: x => x.ShiftId, + principalTable: "RegisterShifts", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_RegisterShifts_BranchId_Status", + table: "RegisterShifts", + columns: new[] { "BranchId", "Status" }); + + migrationBuilder.CreateIndex( + name: "IX_RegisterShifts_CafeId", + table: "RegisterShifts", + column: "CafeId"); + + migrationBuilder.CreateIndex( + name: "IX_RegisterShifts_ClosedByUserId", + table: "RegisterShifts", + column: "ClosedByUserId"); + + migrationBuilder.CreateIndex( + name: "IX_RegisterShifts_OpenedByUserId", + table: "RegisterShifts", + column: "OpenedByUserId"); + + migrationBuilder.CreateIndex( + name: "IX_CashTransactions_BranchId", + table: "CashTransactions", + column: "BranchId"); + + migrationBuilder.CreateIndex( + name: "IX_CashTransactions_CafeId_BranchId", + table: "CashTransactions", + columns: new[] { "CafeId", "BranchId" }); + + migrationBuilder.CreateIndex( + name: "IX_CashTransactions_ShiftId", + table: "CashTransactions", + column: "ShiftId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "CashTransactions"); + + migrationBuilder.DropTable( + name: "RegisterShifts"); + + migrationBuilder.DropForeignKey( + name: "FK_EmployeeSchedules_Employees_EmployeeId", + table: "EmployeeSchedules"); + + migrationBuilder.RenameTable( + name: "EmployeeSchedules", + newName: "Shifts"); + + migrationBuilder.RenameIndex( + name: "IX_EmployeeSchedules_EmployeeId_DayOfWeek", + table: "Shifts", + newName: "IX_Shifts_EmployeeId_DayOfWeek"); + + migrationBuilder.AddForeignKey( + name: "FK_Shifts_Employees_EmployeeId", + table: "Shifts", + column: "EmployeeId", + principalTable: "Employees", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260521210928_AddDailyReport.Designer.cs b/src/Meezi.Infrastructure/Data/Migrations/20260521210928_AddDailyReport.Designer.cs new file mode 100644 index 0000000..46a6128 --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260521210928_AddDailyReport.Designer.cs @@ -0,0 +1,1773 @@ +// +using System; +using System.Collections.Generic; +using Meezi.Core.Entities; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260521210928_AddDailyReport")] + partial class AddDailyReport + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ClockIn") + .HasColumnType("timestamp with time zone"); + + b.Property("ClockOut") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "Date") + .IsUnique(); + + b.ToTable("Attendances"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("City") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Phone") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "IsActive"); + + b.ToTable("Branches"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("CoverImageUrl") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsVerified") + .HasColumnType("boolean"); + + b.Property("LogoUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("PlanExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("PreferredLanguage") + .IsRequired() + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SnappfoodVendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Cafes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AuthorName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("AuthorPhone") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Comment") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerRepliedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerReply") + .HasColumnType("text"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "CreatedAt"); + + b.ToTable("CafeReviews"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CashTransaction", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("ReferenceId") + .HasColumnType("text"); + + b.Property("ShiftId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("ShiftId"); + + b.HasIndex("CafeId", "BranchId"); + + b.ToTable("CashTransactions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MaxDiscount") + .HasColumnType("numeric"); + + b.Property("MinOrderAmount") + .HasColumnType("numeric"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TargetGroup") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UsageLimit") + .HasColumnType("integer"); + + b.Property("UsedCount") + .HasColumnType("integer"); + + b.Property("Value") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Code") + .IsUnique(); + + b.ToTable("Coupons"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BirthDateJalali") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Group") + .HasColumnType("integer"); + + b.Property("LoyaltyPoints") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReferredBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DailyReport", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AvgOrderValue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CardRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CashRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreditRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GeneratedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("NetIncome") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property>("TopProducts") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("TotalExpenses") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TotalOrders") + .HasColumnType("integer"); + + b.Property("TotalRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TotalVoids") + .HasColumnType("integer"); + + b.Property("VoidAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "BranchId", "Date") + .IsUnique(); + + b.ToTable("DailyReports"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("PinCode") + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Employees"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Deductions") + .HasColumnType("numeric"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsPaid") + .HasColumnType("boolean"); + + b.Property("MonthYear") + .IsRequired() + .HasColumnType("text"); + + b.Property("NetSalary") + .HasColumnType("numeric"); + + b.Property("OvertimePay") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "MonthYear") + .IsUnique(); + + b.ToTable("EmployeeSalaries"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSchedule", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DayOfWeek") + .HasColumnType("integer"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShiftType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "DayOfWeek") + .IsUnique(); + + b.ToTable("EmployeeSchedules", (string)null); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("QuantityOnHand") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("ReorderLevel") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("Unit") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Ingredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndDate") + .HasColumnType("date"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("ReviewedBy") + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId"); + + b.ToTable("LeaveRequests"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("TaxId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("TaxId"); + + b.ToTable("MenuCategories"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CategoryId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("CategoryId"); + + b.ToTable("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CouponId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("EmployeeId") + .HasColumnType("text"); + + b.Property("GuestName") + .HasColumnType("text"); + + b.Property("GuestPhone") + .HasColumnType("text"); + + b.Property("OrderType") + .HasColumnType("integer"); + + b.Property("ReservationId") + .HasColumnType("text"); + + b.Property("SnappfoodOrderId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subtotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("TaxTotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Total") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId"); + + b.HasIndex("CouponId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("ReservationId"); + + b.HasIndex("TableId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsVoided") + .HasColumnType("boolean"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UnitPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VoidedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VoidedByUserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Reference") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.QueueTicket", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerLabel") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedByUserId") + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("integer"); + + b.Property("OrderId") + .HasColumnType("text"); + + b.Property("ServiceDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("OrderId"); + + b.HasIndex("CafeId", "BranchId", "ServiceDate", "Number") + .IsUnique(); + + b.ToTable("QueueTickets"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClosedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ClosedByUserId") + .HasColumnType("text"); + + b.Property("ClosingCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Discrepancy") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ExpectedCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("OpenedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OpenedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("OpeningCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("ClosedByUserId"); + + b.HasIndex("OpenedByUserId"); + + b.HasIndex("BranchId", "Status"); + + b.ToTable("RegisterShifts", (string)null); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Delta") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("IngredientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Note") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("IngredientId"); + + b.ToTable("StockMovements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AmountRials") + .HasColumnType("bigint"); + + b.Property("AmountToman") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Authority") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Months") + .HasColumnType("integer"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("RefId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Authority"); + + b.HasIndex("CafeId"); + + b.ToTable("SubscriptionPayments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Floor") + .HasColumnType("text"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsCleaning") + .HasColumnType("boolean"); + + b.Property("Number") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("QrCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId"); + + b.HasIndex("QrCode") + .IsUnique(); + + b.ToTable("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GuestName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GuestPhone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("PartySize") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("time without time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("TableId"); + + b.HasIndex("CafeId", "Date", "Time"); + + b.ToTable("TableReservations"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsCompound") + .HasColumnType("boolean"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("IsRequired") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Attendances") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Branches") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Reviews") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CashTransaction", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Shift", "Shift") + .WithMany("Transactions") + .HasForeignKey("ShiftId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Shift"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Coupons") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Customers") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DailyReport", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Staff") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Employees") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Salaries") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSchedule", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Schedules") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Ingredients") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("LeaveRequests") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuCategories") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Tax", "Tax") + .WithMany("MenuCategories") + .HasForeignKey("TaxId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Tax"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuItems") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuCategory", "Category") + .WithMany("MenuItems") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Orders") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Orders") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Coupon", "Coupon") + .WithMany("Orders") + .HasForeignKey("CouponId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Orders") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.TableReservation", "Reservation") + .WithMany() + .HasForeignKey("ReservationId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany("Orders") + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Coupon"); + + b.Navigation("Customer"); + + b.Navigation("Employee"); + + b.Navigation("Reservation"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("OrderItems") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Items") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MenuItem"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Payments") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.QueueTicket", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Employee", "ClosedBy") + .WithMany() + .HasForeignKey("ClosedByUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "OpenedBy") + .WithMany() + .HasForeignKey("OpenedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("ClosedBy"); + + b.Navigation("OpenedBy"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Ingredient", "Ingredient") + .WithMany("Movements") + .HasForeignKey("IngredientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Ingredient"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("SubscriptionPayments") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Tables") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Tables") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany() + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Customer"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Taxes") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Navigation("Orders"); + + b.Navigation("Staff"); + + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Navigation("Branches"); + + b.Navigation("Coupons"); + + b.Navigation("Customers"); + + b.Navigation("Employees"); + + b.Navigation("Ingredients"); + + b.Navigation("MenuCategories"); + + b.Navigation("MenuItems"); + + b.Navigation("Orders"); + + b.Navigation("Reviews"); + + b.Navigation("SubscriptionPayments"); + + b.Navigation("Tables"); + + b.Navigation("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Navigation("Attendances"); + + b.Navigation("LeaveRequests"); + + b.Navigation("Orders"); + + b.Navigation("Salaries"); + + b.Navigation("Schedules"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Navigation("Movements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Navigation("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Navigation("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Navigation("Items"); + + b.Navigation("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Navigation("MenuCategories"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260521210928_AddDailyReport.cs b/src/Meezi.Infrastructure/Data/Migrations/20260521210928_AddDailyReport.cs new file mode 100644 index 0000000..9fba72b --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260521210928_AddDailyReport.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using Meezi.Core.Entities; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + /// + public partial class AddDailyReport : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "DailyReports", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + BranchId = table.Column(type: "text", nullable: false), + Date = table.Column(type: "date", nullable: false), + TotalRevenue = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + CashRevenue = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + CardRevenue = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + CreditRevenue = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + TotalOrders = table.Column(type: "integer", nullable: false), + AvgOrderValue = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + TotalVoids = table.Column(type: "integer", nullable: false), + VoidAmount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + TotalExpenses = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + NetIncome = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + TopProducts = table.Column>(type: "jsonb", nullable: false), + GeneratedAt = table.Column(type: "timestamp with time zone", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CafeId = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_DailyReports", x => x.Id); + table.ForeignKey( + name: "FK_DailyReports_Branches_BranchId", + column: x => x.BranchId, + principalTable: "Branches", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_DailyReports_BranchId", + table: "DailyReports", + column: "BranchId"); + + migrationBuilder.CreateIndex( + name: "IX_DailyReports_CafeId_BranchId_Date", + table: "DailyReports", + columns: new[] { "CafeId", "BranchId", "Date" }, + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "DailyReports"); + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260522031000_AddExpense.Designer.cs b/src/Meezi.Infrastructure/Data/Migrations/20260522031000_AddExpense.Designer.cs new file mode 100644 index 0000000..f7e7623 --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260522031000_AddExpense.Designer.cs @@ -0,0 +1,1841 @@ +// +using System; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260522031000_AddExpense")] + partial class AddExpense + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ClockIn") + .HasColumnType("timestamp with time zone"); + + b.Property("ClockOut") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "Date") + .IsUnique(); + + b.ToTable("Attendances"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("City") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Phone") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "IsActive"); + + b.ToTable("Branches"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("CoverImageUrl") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsVerified") + .HasColumnType("boolean"); + + b.Property("LogoUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("PlanExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("PreferredLanguage") + .IsRequired() + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SnappfoodVendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Cafes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AuthorName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("AuthorPhone") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Comment") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerRepliedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerReply") + .HasColumnType("text"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "CreatedAt"); + + b.ToTable("CafeReviews"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CashTransaction", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("ReferenceId") + .HasColumnType("text"); + + b.Property("ShiftId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("ShiftId"); + + b.HasIndex("CafeId", "BranchId"); + + b.ToTable("CashTransactions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MaxDiscount") + .HasColumnType("numeric"); + + b.Property("MinOrderAmount") + .HasColumnType("numeric"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TargetGroup") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UsageLimit") + .HasColumnType("integer"); + + b.Property("UsedCount") + .HasColumnType("integer"); + + b.Property("Value") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Code") + .IsUnique(); + + b.ToTable("Coupons"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BirthDateJalali") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Group") + .HasColumnType("integer"); + + b.Property("LoyaltyPoints") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReferredBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DailyReport", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AvgOrderValue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CardRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CashRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreditRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GeneratedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("NetIncome") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TopProducts") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("TotalExpenses") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TotalOrders") + .HasColumnType("integer"); + + b.Property("TotalRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TotalVoids") + .HasColumnType("integer"); + + b.Property("VoidAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "BranchId", "Date") + .IsUnique(); + + b.ToTable("DailyReports"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("PinCode") + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Employees"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Deductions") + .HasColumnType("numeric"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsPaid") + .HasColumnType("boolean"); + + b.Property("MonthYear") + .IsRequired() + .HasColumnType("text"); + + b.Property("NetSalary") + .HasColumnType("numeric"); + + b.Property("OvertimePay") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "MonthYear") + .IsUnique(); + + b.ToTable("EmployeeSalaries"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSchedule", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DayOfWeek") + .HasColumnType("integer"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShiftType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "DayOfWeek") + .IsUnique(); + + b.ToTable("EmployeeSchedules", (string)null); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Expense", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Category") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptImageUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ShiftId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("ShiftId"); + + b.HasIndex("CafeId", "BranchId", "CreatedAt"); + + b.ToTable("Expenses"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("QuantityOnHand") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("ReorderLevel") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("Unit") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Ingredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndDate") + .HasColumnType("date"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("ReviewedBy") + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId"); + + b.ToTable("LeaveRequests"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("TaxId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("TaxId"); + + b.ToTable("MenuCategories"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CategoryId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("CategoryId"); + + b.ToTable("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CouponId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("EmployeeId") + .HasColumnType("text"); + + b.Property("GuestName") + .HasColumnType("text"); + + b.Property("GuestPhone") + .HasColumnType("text"); + + b.Property("OrderType") + .HasColumnType("integer"); + + b.Property("ReservationId") + .HasColumnType("text"); + + b.Property("SnappfoodOrderId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subtotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("TaxTotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Total") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId"); + + b.HasIndex("CouponId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("ReservationId"); + + b.HasIndex("TableId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsVoided") + .HasColumnType("boolean"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UnitPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VoidedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VoidedByUserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Reference") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.QueueTicket", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerLabel") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedByUserId") + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("integer"); + + b.Property("OrderId") + .HasColumnType("text"); + + b.Property("ServiceDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("OrderId"); + + b.HasIndex("CafeId", "BranchId", "ServiceDate", "Number") + .IsUnique(); + + b.ToTable("QueueTickets"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClosedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ClosedByUserId") + .HasColumnType("text"); + + b.Property("ClosingCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Discrepancy") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ExpectedCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("OpenedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OpenedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("OpeningCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("ClosedByUserId"); + + b.HasIndex("OpenedByUserId"); + + b.HasIndex("BranchId", "Status"); + + b.ToTable("RegisterShifts", (string)null); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Delta") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("IngredientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Note") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("IngredientId"); + + b.ToTable("StockMovements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AmountRials") + .HasColumnType("bigint"); + + b.Property("AmountToman") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Authority") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Months") + .HasColumnType("integer"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("RefId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Authority"); + + b.HasIndex("CafeId"); + + b.ToTable("SubscriptionPayments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Floor") + .HasColumnType("text"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsCleaning") + .HasColumnType("boolean"); + + b.Property("Number") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("QrCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId"); + + b.HasIndex("QrCode") + .IsUnique(); + + b.ToTable("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GuestName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GuestPhone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("PartySize") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("time without time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("TableId"); + + b.HasIndex("CafeId", "Date", "Time"); + + b.ToTable("TableReservations"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsCompound") + .HasColumnType("boolean"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("IsRequired") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Attendances") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Branches") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Reviews") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CashTransaction", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Shift", "Shift") + .WithMany("Transactions") + .HasForeignKey("ShiftId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Shift"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Coupons") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Customers") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DailyReport", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Staff") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Employees") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Salaries") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSchedule", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Schedules") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Expense", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Shift", "Shift") + .WithMany() + .HasForeignKey("ShiftId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Shift"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Ingredients") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("LeaveRequests") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuCategories") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Tax", "Tax") + .WithMany("MenuCategories") + .HasForeignKey("TaxId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Tax"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuItems") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuCategory", "Category") + .WithMany("MenuItems") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Orders") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Orders") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Coupon", "Coupon") + .WithMany("Orders") + .HasForeignKey("CouponId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Orders") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.TableReservation", "Reservation") + .WithMany() + .HasForeignKey("ReservationId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany("Orders") + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Coupon"); + + b.Navigation("Customer"); + + b.Navigation("Employee"); + + b.Navigation("Reservation"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("OrderItems") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Items") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MenuItem"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Payments") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.QueueTicket", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Employee", "ClosedBy") + .WithMany() + .HasForeignKey("ClosedByUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "OpenedBy") + .WithMany() + .HasForeignKey("OpenedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("ClosedBy"); + + b.Navigation("OpenedBy"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Ingredient", "Ingredient") + .WithMany("Movements") + .HasForeignKey("IngredientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Ingredient"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("SubscriptionPayments") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Tables") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Tables") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany() + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Customer"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Taxes") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Navigation("Orders"); + + b.Navigation("Staff"); + + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Navigation("Branches"); + + b.Navigation("Coupons"); + + b.Navigation("Customers"); + + b.Navigation("Employees"); + + b.Navigation("Ingredients"); + + b.Navigation("MenuCategories"); + + b.Navigation("MenuItems"); + + b.Navigation("Orders"); + + b.Navigation("Reviews"); + + b.Navigation("SubscriptionPayments"); + + b.Navigation("Tables"); + + b.Navigation("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Navigation("Attendances"); + + b.Navigation("LeaveRequests"); + + b.Navigation("Orders"); + + b.Navigation("Salaries"); + + b.Navigation("Schedules"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Navigation("Movements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Navigation("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Navigation("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Navigation("Items"); + + b.Navigation("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Navigation("MenuCategories"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260522031000_AddExpense.cs b/src/Meezi.Infrastructure/Data/Migrations/20260522031000_AddExpense.cs new file mode 100644 index 0000000..814fb08 --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260522031000_AddExpense.cs @@ -0,0 +1,70 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + /// + public partial class AddExpense : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Expenses", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + BranchId = table.Column(type: "text", nullable: false), + ShiftId = table.Column(type: "text", nullable: true), + Category = table.Column(type: "integer", nullable: false), + Amount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + Note = table.Column(type: "character varying(500)", maxLength: 500, nullable: true), + ReceiptImageUrl = table.Column(type: "character varying(500)", maxLength: 500, nullable: true), + CreatedByUserId = table.Column(type: "text", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CafeId = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Expenses", x => x.Id); + table.ForeignKey( + name: "FK_Expenses_Branches_BranchId", + column: x => x.BranchId, + principalTable: "Branches", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Expenses_RegisterShifts_ShiftId", + column: x => x.ShiftId, + principalTable: "RegisterShifts", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateIndex( + name: "IX_Expenses_BranchId", + table: "Expenses", + column: "BranchId"); + + migrationBuilder.CreateIndex( + name: "IX_Expenses_CafeId_BranchId_CreatedAt", + table: "Expenses", + columns: new[] { "CafeId", "BranchId", "CreatedAt" }); + + migrationBuilder.CreateIndex( + name: "IX_Expenses_ShiftId", + table: "Expenses", + column: "ShiftId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Expenses"); + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260522040108_BranchMenuItemOverride.Designer.cs b/src/Meezi.Infrastructure/Data/Migrations/20260522040108_BranchMenuItemOverride.Designer.cs new file mode 100644 index 0000000..7a529bb --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260522040108_BranchMenuItemOverride.Designer.cs @@ -0,0 +1,1913 @@ +// +using System; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260522040108_BranchMenuItemOverride")] + partial class BranchMenuItemOverride + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ClockIn") + .HasColumnType("timestamp with time zone"); + + b.Property("ClockOut") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "Date") + .IsUnique(); + + b.ToTable("Attendances"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("City") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Phone") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "IsActive"); + + b.ToTable("Branches"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.BranchMenuItemOverride", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("PriceOverride") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("SortOrderOverride") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedByUserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("BranchId", "MenuItemId") + .IsUnique(); + + b.ToTable("BranchMenuItemOverrides"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("CoverImageUrl") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsVerified") + .HasColumnType("boolean"); + + b.Property("LogoUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("PlanExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("PreferredLanguage") + .IsRequired() + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SnappfoodVendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Cafes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AuthorName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("AuthorPhone") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Comment") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerRepliedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerReply") + .HasColumnType("text"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "CreatedAt"); + + b.ToTable("CafeReviews"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CashTransaction", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("ReferenceId") + .HasColumnType("text"); + + b.Property("ShiftId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("ShiftId"); + + b.HasIndex("CafeId", "BranchId"); + + b.ToTable("CashTransactions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MaxDiscount") + .HasColumnType("numeric"); + + b.Property("MinOrderAmount") + .HasColumnType("numeric"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TargetGroup") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UsageLimit") + .HasColumnType("integer"); + + b.Property("UsedCount") + .HasColumnType("integer"); + + b.Property("Value") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Code") + .IsUnique(); + + b.ToTable("Coupons"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BirthDateJalali") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Group") + .HasColumnType("integer"); + + b.Property("LoyaltyPoints") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReferredBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DailyReport", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AvgOrderValue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CardRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CashRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreditRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GeneratedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("NetIncome") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TopProducts") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("TotalExpenses") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TotalOrders") + .HasColumnType("integer"); + + b.Property("TotalRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TotalVoids") + .HasColumnType("integer"); + + b.Property("VoidAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "BranchId", "Date") + .IsUnique(); + + b.ToTable("DailyReports"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("PinCode") + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Employees"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Deductions") + .HasColumnType("numeric"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsPaid") + .HasColumnType("boolean"); + + b.Property("MonthYear") + .IsRequired() + .HasColumnType("text"); + + b.Property("NetSalary") + .HasColumnType("numeric"); + + b.Property("OvertimePay") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "MonthYear") + .IsUnique(); + + b.ToTable("EmployeeSalaries"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSchedule", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DayOfWeek") + .HasColumnType("integer"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShiftType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "DayOfWeek") + .IsUnique(); + + b.ToTable("EmployeeSchedules", (string)null); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Expense", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Category") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptImageUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ShiftId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("ShiftId"); + + b.HasIndex("CafeId", "BranchId", "CreatedAt"); + + b.ToTable("Expenses"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("QuantityOnHand") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("ReorderLevel") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("Unit") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Ingredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndDate") + .HasColumnType("date"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("ReviewedBy") + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId"); + + b.ToTable("LeaveRequests"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("TaxId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("TaxId"); + + b.ToTable("MenuCategories"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CategoryId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("CategoryId"); + + b.ToTable("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CouponId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("EmployeeId") + .HasColumnType("text"); + + b.Property("GuestName") + .HasColumnType("text"); + + b.Property("GuestPhone") + .HasColumnType("text"); + + b.Property("OrderType") + .HasColumnType("integer"); + + b.Property("ReservationId") + .HasColumnType("text"); + + b.Property("SnappfoodOrderId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subtotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("TaxTotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Total") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId"); + + b.HasIndex("CouponId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("ReservationId"); + + b.HasIndex("TableId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsVoided") + .HasColumnType("boolean"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UnitPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VoidedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VoidedByUserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Reference") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.QueueTicket", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerLabel") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedByUserId") + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("integer"); + + b.Property("OrderId") + .HasColumnType("text"); + + b.Property("ServiceDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("OrderId"); + + b.HasIndex("CafeId", "BranchId", "ServiceDate", "Number") + .IsUnique(); + + b.ToTable("QueueTickets"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClosedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ClosedByUserId") + .HasColumnType("text"); + + b.Property("ClosingCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Discrepancy") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ExpectedCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("OpenedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OpenedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("OpeningCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("ClosedByUserId"); + + b.HasIndex("OpenedByUserId"); + + b.HasIndex("BranchId", "Status"); + + b.ToTable("RegisterShifts", (string)null); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Delta") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("IngredientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Note") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("IngredientId"); + + b.ToTable("StockMovements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AmountRials") + .HasColumnType("bigint"); + + b.Property("AmountToman") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Authority") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Months") + .HasColumnType("integer"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("RefId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Authority"); + + b.HasIndex("CafeId"); + + b.ToTable("SubscriptionPayments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Floor") + .HasColumnType("text"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsCleaning") + .HasColumnType("boolean"); + + b.Property("Number") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("QrCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId"); + + b.HasIndex("QrCode") + .IsUnique(); + + b.ToTable("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GuestName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GuestPhone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("PartySize") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("time without time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("TableId"); + + b.HasIndex("CafeId", "Date", "Time"); + + b.ToTable("TableReservations"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsCompound") + .HasColumnType("boolean"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("IsRequired") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Attendances") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Branches") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.BranchMenuItemOverride", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("BranchOverrides") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("MenuItem"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Reviews") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CashTransaction", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Shift", "Shift") + .WithMany("Transactions") + .HasForeignKey("ShiftId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Shift"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Coupons") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Customers") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DailyReport", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Staff") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Employees") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Salaries") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSchedule", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Schedules") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Expense", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Shift", "Shift") + .WithMany() + .HasForeignKey("ShiftId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Shift"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Ingredients") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("LeaveRequests") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuCategories") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Tax", "Tax") + .WithMany("MenuCategories") + .HasForeignKey("TaxId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Tax"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuItems") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuCategory", "Category") + .WithMany("MenuItems") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Orders") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Orders") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Coupon", "Coupon") + .WithMany("Orders") + .HasForeignKey("CouponId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Orders") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.TableReservation", "Reservation") + .WithMany() + .HasForeignKey("ReservationId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany("Orders") + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Coupon"); + + b.Navigation("Customer"); + + b.Navigation("Employee"); + + b.Navigation("Reservation"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("OrderItems") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Items") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MenuItem"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Payments") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.QueueTicket", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Employee", "ClosedBy") + .WithMany() + .HasForeignKey("ClosedByUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "OpenedBy") + .WithMany() + .HasForeignKey("OpenedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("ClosedBy"); + + b.Navigation("OpenedBy"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Ingredient", "Ingredient") + .WithMany("Movements") + .HasForeignKey("IngredientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Ingredient"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("SubscriptionPayments") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Tables") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Tables") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany() + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Customer"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Taxes") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Navigation("Orders"); + + b.Navigation("Staff"); + + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Navigation("Branches"); + + b.Navigation("Coupons"); + + b.Navigation("Customers"); + + b.Navigation("Employees"); + + b.Navigation("Ingredients"); + + b.Navigation("MenuCategories"); + + b.Navigation("MenuItems"); + + b.Navigation("Orders"); + + b.Navigation("Reviews"); + + b.Navigation("SubscriptionPayments"); + + b.Navigation("Tables"); + + b.Navigation("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Navigation("Attendances"); + + b.Navigation("LeaveRequests"); + + b.Navigation("Orders"); + + b.Navigation("Salaries"); + + b.Navigation("Schedules"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Navigation("Movements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Navigation("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Navigation("BranchOverrides"); + + b.Navigation("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Navigation("Items"); + + b.Navigation("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Navigation("MenuCategories"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260522040108_BranchMenuItemOverride.cs b/src/Meezi.Infrastructure/Data/Migrations/20260522040108_BranchMenuItemOverride.cs new file mode 100644 index 0000000..a00aac7 --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260522040108_BranchMenuItemOverride.cs @@ -0,0 +1,71 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + /// + public partial class BranchMenuItemOverride : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "BranchMenuItemOverrides", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + BranchId = table.Column(type: "text", nullable: false), + MenuItemId = table.Column(type: "text", nullable: false), + IsAvailable = table.Column(type: "boolean", nullable: false), + PriceOverride = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: true), + SortOrderOverride = table.Column(type: "integer", nullable: true), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedByUserId = table.Column(type: "text", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CafeId = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_BranchMenuItemOverrides", x => x.Id); + table.ForeignKey( + name: "FK_BranchMenuItemOverrides_Branches_BranchId", + column: x => x.BranchId, + principalTable: "Branches", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_BranchMenuItemOverrides_MenuItems_MenuItemId", + column: x => x.MenuItemId, + principalTable: "MenuItems", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_BranchMenuItemOverrides_BranchId_MenuItemId", + table: "BranchMenuItemOverrides", + columns: new[] { "BranchId", "MenuItemId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_BranchMenuItemOverrides_CafeId", + table: "BranchMenuItemOverrides", + column: "CafeId"); + + migrationBuilder.CreateIndex( + name: "IX_BranchMenuItemOverrides_MenuItemId", + table: "BranchMenuItemOverrides", + column: "MenuItemId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "BranchMenuItemOverrides"); + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260522043503_AddPrinterSettings.Designer.cs b/src/Meezi.Infrastructure/Data/Migrations/20260522043503_AddPrinterSettings.Designer.cs new file mode 100644 index 0000000..c442c71 --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260522043503_AddPrinterSettings.Designer.cs @@ -0,0 +1,1945 @@ +// +using System; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260522043503_AddPrinterSettings")] + partial class AddPrinterSettings + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ClockIn") + .HasColumnType("timestamp with time zone"); + + b.Property("ClockOut") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "Date") + .IsUnique(); + + b.ToTable("Attendances"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("AutoCutEnabled") + .HasColumnType("boolean"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("City") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("KitchenPrinterIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("KitchenPrinterPort") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("PaperWidthMm") + .HasColumnType("integer"); + + b.Property("Phone") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("ReceiptFooter") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptHeader") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptPrinterIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("ReceiptPrinterPort") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WifiPassword") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "IsActive"); + + b.ToTable("Branches"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.BranchMenuItemOverride", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("PriceOverride") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("SortOrderOverride") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedByUserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("BranchId", "MenuItemId") + .IsUnique(); + + b.ToTable("BranchMenuItemOverrides"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("CoverImageUrl") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsVerified") + .HasColumnType("boolean"); + + b.Property("LogoUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("PlanExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("PreferredLanguage") + .IsRequired() + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SnappfoodVendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Cafes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AuthorName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("AuthorPhone") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Comment") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerRepliedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerReply") + .HasColumnType("text"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "CreatedAt"); + + b.ToTable("CafeReviews"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CashTransaction", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("ReferenceId") + .HasColumnType("text"); + + b.Property("ShiftId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("ShiftId"); + + b.HasIndex("CafeId", "BranchId"); + + b.ToTable("CashTransactions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MaxDiscount") + .HasColumnType("numeric"); + + b.Property("MinOrderAmount") + .HasColumnType("numeric"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TargetGroup") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UsageLimit") + .HasColumnType("integer"); + + b.Property("UsedCount") + .HasColumnType("integer"); + + b.Property("Value") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Code") + .IsUnique(); + + b.ToTable("Coupons"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BirthDateJalali") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Group") + .HasColumnType("integer"); + + b.Property("LoyaltyPoints") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReferredBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DailyReport", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AvgOrderValue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CardRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CashRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreditRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GeneratedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("NetIncome") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TopProducts") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("TotalExpenses") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TotalOrders") + .HasColumnType("integer"); + + b.Property("TotalRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TotalVoids") + .HasColumnType("integer"); + + b.Property("VoidAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "BranchId", "Date") + .IsUnique(); + + b.ToTable("DailyReports"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("PinCode") + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Employees"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Deductions") + .HasColumnType("numeric"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsPaid") + .HasColumnType("boolean"); + + b.Property("MonthYear") + .IsRequired() + .HasColumnType("text"); + + b.Property("NetSalary") + .HasColumnType("numeric"); + + b.Property("OvertimePay") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "MonthYear") + .IsUnique(); + + b.ToTable("EmployeeSalaries"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSchedule", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DayOfWeek") + .HasColumnType("integer"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShiftType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "DayOfWeek") + .IsUnique(); + + b.ToTable("EmployeeSchedules", (string)null); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Expense", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Category") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptImageUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ShiftId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("ShiftId"); + + b.HasIndex("CafeId", "BranchId", "CreatedAt"); + + b.ToTable("Expenses"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("QuantityOnHand") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("ReorderLevel") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("Unit") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Ingredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndDate") + .HasColumnType("date"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("ReviewedBy") + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId"); + + b.ToTable("LeaveRequests"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("TaxId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("TaxId"); + + b.ToTable("MenuCategories"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CategoryId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("CategoryId"); + + b.ToTable("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CouponId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("EmployeeId") + .HasColumnType("text"); + + b.Property("GuestName") + .HasColumnType("text"); + + b.Property("GuestPhone") + .HasColumnType("text"); + + b.Property("OrderType") + .HasColumnType("integer"); + + b.Property("ReservationId") + .HasColumnType("text"); + + b.Property("SnappfoodOrderId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subtotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("TaxTotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Total") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId"); + + b.HasIndex("CouponId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("ReservationId"); + + b.HasIndex("TableId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsVoided") + .HasColumnType("boolean"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UnitPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VoidedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VoidedByUserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Reference") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.QueueTicket", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerLabel") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedByUserId") + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("integer"); + + b.Property("OrderId") + .HasColumnType("text"); + + b.Property("ServiceDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("OrderId"); + + b.HasIndex("CafeId", "BranchId", "ServiceDate", "Number") + .IsUnique(); + + b.ToTable("QueueTickets"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClosedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ClosedByUserId") + .HasColumnType("text"); + + b.Property("ClosingCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Discrepancy") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ExpectedCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("OpenedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OpenedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("OpeningCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("ClosedByUserId"); + + b.HasIndex("OpenedByUserId"); + + b.HasIndex("BranchId", "Status"); + + b.ToTable("RegisterShifts", (string)null); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Delta") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("IngredientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Note") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("IngredientId"); + + b.ToTable("StockMovements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AmountRials") + .HasColumnType("bigint"); + + b.Property("AmountToman") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Authority") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Months") + .HasColumnType("integer"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("RefId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Authority"); + + b.HasIndex("CafeId"); + + b.ToTable("SubscriptionPayments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Floor") + .HasColumnType("text"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsCleaning") + .HasColumnType("boolean"); + + b.Property("Number") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("QrCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId"); + + b.HasIndex("QrCode") + .IsUnique(); + + b.ToTable("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GuestName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GuestPhone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("PartySize") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("time without time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("TableId"); + + b.HasIndex("CafeId", "Date", "Time"); + + b.ToTable("TableReservations"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsCompound") + .HasColumnType("boolean"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("IsRequired") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Attendances") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Branches") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.BranchMenuItemOverride", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("BranchOverrides") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("MenuItem"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Reviews") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CashTransaction", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Shift", "Shift") + .WithMany("Transactions") + .HasForeignKey("ShiftId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Shift"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Coupons") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Customers") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DailyReport", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Staff") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Employees") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Salaries") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSchedule", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Schedules") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Expense", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Shift", "Shift") + .WithMany() + .HasForeignKey("ShiftId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Shift"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Ingredients") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("LeaveRequests") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuCategories") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Tax", "Tax") + .WithMany("MenuCategories") + .HasForeignKey("TaxId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Tax"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuItems") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuCategory", "Category") + .WithMany("MenuItems") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Orders") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Orders") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Coupon", "Coupon") + .WithMany("Orders") + .HasForeignKey("CouponId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Orders") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.TableReservation", "Reservation") + .WithMany() + .HasForeignKey("ReservationId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany("Orders") + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Coupon"); + + b.Navigation("Customer"); + + b.Navigation("Employee"); + + b.Navigation("Reservation"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("OrderItems") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Items") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MenuItem"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Payments") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.QueueTicket", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Employee", "ClosedBy") + .WithMany() + .HasForeignKey("ClosedByUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "OpenedBy") + .WithMany() + .HasForeignKey("OpenedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("ClosedBy"); + + b.Navigation("OpenedBy"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Ingredient", "Ingredient") + .WithMany("Movements") + .HasForeignKey("IngredientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Ingredient"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("SubscriptionPayments") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Tables") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Tables") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany() + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Customer"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Taxes") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Navigation("Orders"); + + b.Navigation("Staff"); + + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Navigation("Branches"); + + b.Navigation("Coupons"); + + b.Navigation("Customers"); + + b.Navigation("Employees"); + + b.Navigation("Ingredients"); + + b.Navigation("MenuCategories"); + + b.Navigation("MenuItems"); + + b.Navigation("Orders"); + + b.Navigation("Reviews"); + + b.Navigation("SubscriptionPayments"); + + b.Navigation("Tables"); + + b.Navigation("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Navigation("Attendances"); + + b.Navigation("LeaveRequests"); + + b.Navigation("Orders"); + + b.Navigation("Salaries"); + + b.Navigation("Schedules"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Navigation("Movements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Navigation("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Navigation("BranchOverrides"); + + b.Navigation("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Navigation("Items"); + + b.Navigation("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Navigation("MenuCategories"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260522043503_AddPrinterSettings.cs b/src/Meezi.Infrastructure/Data/Migrations/20260522043503_AddPrinterSettings.cs new file mode 100644 index 0000000..a748cbf --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260522043503_AddPrinterSettings.cs @@ -0,0 +1,115 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + /// + public partial class AddPrinterSettings : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "AutoCutEnabled", + table: "Branches", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "KitchenPrinterIp", + table: "Branches", + type: "character varying(45)", + maxLength: 45, + nullable: true); + + migrationBuilder.AddColumn( + name: "KitchenPrinterPort", + table: "Branches", + type: "integer", + nullable: true); + + migrationBuilder.AddColumn( + name: "PaperWidthMm", + table: "Branches", + type: "integer", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "ReceiptFooter", + table: "Branches", + type: "character varying(500)", + maxLength: 500, + nullable: true); + + migrationBuilder.AddColumn( + name: "ReceiptHeader", + table: "Branches", + type: "character varying(500)", + maxLength: 500, + nullable: true); + + migrationBuilder.AddColumn( + name: "ReceiptPrinterIp", + table: "Branches", + type: "character varying(45)", + maxLength: 45, + nullable: true); + + migrationBuilder.AddColumn( + name: "ReceiptPrinterPort", + table: "Branches", + type: "integer", + nullable: true); + + migrationBuilder.AddColumn( + name: "WifiPassword", + table: "Branches", + type: "character varying(100)", + maxLength: 100, + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "AutoCutEnabled", + table: "Branches"); + + migrationBuilder.DropColumn( + name: "KitchenPrinterIp", + table: "Branches"); + + migrationBuilder.DropColumn( + name: "KitchenPrinterPort", + table: "Branches"); + + migrationBuilder.DropColumn( + name: "PaperWidthMm", + table: "Branches"); + + migrationBuilder.DropColumn( + name: "ReceiptFooter", + table: "Branches"); + + migrationBuilder.DropColumn( + name: "ReceiptHeader", + table: "Branches"); + + migrationBuilder.DropColumn( + name: "ReceiptPrinterIp", + table: "Branches"); + + migrationBuilder.DropColumn( + name: "ReceiptPrinterPort", + table: "Branches"); + + migrationBuilder.DropColumn( + name: "WifiPassword", + table: "Branches"); + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260522044043_BranchTableOwnership.Designer.cs b/src/Meezi.Infrastructure/Data/Migrations/20260522044043_BranchTableOwnership.Designer.cs new file mode 100644 index 0000000..a6dc88e --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260522044043_BranchTableOwnership.Designer.cs @@ -0,0 +1,2021 @@ +// +using System; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260522044043_BranchTableOwnership")] + partial class BranchTableOwnership + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ClockIn") + .HasColumnType("timestamp with time zone"); + + b.Property("ClockOut") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "Date") + .IsUnique(); + + b.ToTable("Attendances"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("AutoCutEnabled") + .HasColumnType("boolean"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("City") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("KitchenPrinterIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("KitchenPrinterPort") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("PaperWidthMm") + .HasColumnType("integer"); + + b.Property("Phone") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("ReceiptFooter") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptHeader") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptPrinterIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("ReceiptPrinterPort") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WifiPassword") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "IsActive"); + + b.ToTable("Branches"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.BranchMenuItemOverride", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("PriceOverride") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("SortOrderOverride") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedByUserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("BranchId", "MenuItemId") + .IsUnique(); + + b.ToTable("BranchMenuItemOverrides"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("CoverImageUrl") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsVerified") + .HasColumnType("boolean"); + + b.Property("LogoUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("PlanExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("PreferredLanguage") + .IsRequired() + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SnappfoodVendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Cafes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AuthorName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("AuthorPhone") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Comment") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerRepliedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerReply") + .HasColumnType("text"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "CreatedAt"); + + b.ToTable("CafeReviews"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CashTransaction", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("ReferenceId") + .HasColumnType("text"); + + b.Property("ShiftId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("ShiftId"); + + b.HasIndex("CafeId", "BranchId"); + + b.ToTable("CashTransactions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MaxDiscount") + .HasColumnType("numeric"); + + b.Property("MinOrderAmount") + .HasColumnType("numeric"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TargetGroup") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UsageLimit") + .HasColumnType("integer"); + + b.Property("UsedCount") + .HasColumnType("integer"); + + b.Property("Value") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Code") + .IsUnique(); + + b.ToTable("Coupons"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BirthDateJalali") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Group") + .HasColumnType("integer"); + + b.Property("LoyaltyPoints") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReferredBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DailyReport", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AvgOrderValue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CardRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CashRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreditRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GeneratedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("NetIncome") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TopProducts") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("TotalExpenses") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TotalOrders") + .HasColumnType("integer"); + + b.Property("TotalRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TotalVoids") + .HasColumnType("integer"); + + b.Property("VoidAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "BranchId", "Date") + .IsUnique(); + + b.ToTable("DailyReports"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("PinCode") + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Employees"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Deductions") + .HasColumnType("numeric"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsPaid") + .HasColumnType("boolean"); + + b.Property("MonthYear") + .IsRequired() + .HasColumnType("text"); + + b.Property("NetSalary") + .HasColumnType("numeric"); + + b.Property("OvertimePay") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "MonthYear") + .IsUnique(); + + b.ToTable("EmployeeSalaries"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSchedule", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DayOfWeek") + .HasColumnType("integer"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShiftType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "DayOfWeek") + .IsUnique(); + + b.ToTable("EmployeeSchedules", (string)null); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Expense", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Category") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptImageUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ShiftId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("ShiftId"); + + b.HasIndex("CafeId", "BranchId", "CreatedAt"); + + b.ToTable("Expenses"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("QuantityOnHand") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("ReorderLevel") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("Unit") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Ingredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndDate") + .HasColumnType("date"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("ReviewedBy") + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId"); + + b.ToTable("LeaveRequests"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("TaxId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("TaxId"); + + b.ToTable("MenuCategories"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CategoryId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("CategoryId"); + + b.ToTable("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CouponId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("EmployeeId") + .HasColumnType("text"); + + b.Property("GuestName") + .HasColumnType("text"); + + b.Property("GuestPhone") + .HasColumnType("text"); + + b.Property("OrderType") + .HasColumnType("integer"); + + b.Property("ReservationId") + .HasColumnType("text"); + + b.Property("SnappfoodOrderId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subtotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("TaxTotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Total") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId"); + + b.HasIndex("CouponId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("ReservationId"); + + b.HasIndex("TableId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsVoided") + .HasColumnType("boolean"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UnitPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VoidedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VoidedByUserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Reference") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.QueueTicket", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerLabel") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedByUserId") + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("integer"); + + b.Property("OrderId") + .HasColumnType("text"); + + b.Property("ServiceDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("OrderId"); + + b.HasIndex("CafeId", "BranchId", "ServiceDate", "Number") + .IsUnique(); + + b.ToTable("QueueTickets"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClosedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ClosedByUserId") + .HasColumnType("text"); + + b.Property("ClosingCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Discrepancy") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ExpectedCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("OpenedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OpenedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("OpeningCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("ClosedByUserId"); + + b.HasIndex("OpenedByUserId"); + + b.HasIndex("BranchId", "Status"); + + b.ToTable("RegisterShifts", (string)null); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Delta") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("IngredientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Note") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("IngredientId"); + + b.ToTable("StockMovements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AmountRials") + .HasColumnType("bigint"); + + b.Property("AmountToman") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Authority") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Months") + .HasColumnType("integer"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("RefId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Authority"); + + b.HasIndex("CafeId"); + + b.ToTable("SubscriptionPayments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Floor") + .HasColumnType("text"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsCleaning") + .HasColumnType("boolean"); + + b.Property("Number") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("QrCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("SectionId") + .HasColumnType("text"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("QrCode") + .IsUnique(); + + b.HasIndex("SectionId"); + + b.HasIndex("BranchId", "SectionId", "SortOrder"); + + b.ToTable("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GuestName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GuestPhone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("PartySize") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("time without time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("TableId"); + + b.HasIndex("CafeId", "Date", "Time"); + + b.ToTable("TableReservations"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("BranchId", "Name"); + + b.ToTable("TableSections"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsCompound") + .HasColumnType("boolean"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("IsRequired") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Attendances") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Branches") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.BranchMenuItemOverride", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("BranchOverrides") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("MenuItem"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Reviews") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CashTransaction", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Shift", "Shift") + .WithMany("Transactions") + .HasForeignKey("ShiftId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Shift"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Coupons") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Customers") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DailyReport", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Staff") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Employees") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Salaries") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSchedule", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Schedules") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Expense", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Shift", "Shift") + .WithMany() + .HasForeignKey("ShiftId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Shift"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Ingredients") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("LeaveRequests") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuCategories") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Tax", "Tax") + .WithMany("MenuCategories") + .HasForeignKey("TaxId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Tax"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuItems") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuCategory", "Category") + .WithMany("MenuItems") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Orders") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Orders") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Coupon", "Coupon") + .WithMany("Orders") + .HasForeignKey("CouponId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Orders") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.TableReservation", "Reservation") + .WithMany() + .HasForeignKey("ReservationId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany("Orders") + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Coupon"); + + b.Navigation("Customer"); + + b.Navigation("Employee"); + + b.Navigation("Reservation"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("OrderItems") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Items") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MenuItem"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Payments") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.QueueTicket", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Employee", "ClosedBy") + .WithMany() + .HasForeignKey("ClosedByUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "OpenedBy") + .WithMany() + .HasForeignKey("OpenedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("ClosedBy"); + + b.Navigation("OpenedBy"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Ingredient", "Ingredient") + .WithMany("Movements") + .HasForeignKey("IngredientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Ingredient"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("SubscriptionPayments") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Tables") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Tables") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.TableSection", "Section") + .WithMany("Tables") + .HasForeignKey("SectionId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Section"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany() + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Customer"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Sections") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Taxes") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Navigation("Orders"); + + b.Navigation("Sections"); + + b.Navigation("Staff"); + + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Navigation("Branches"); + + b.Navigation("Coupons"); + + b.Navigation("Customers"); + + b.Navigation("Employees"); + + b.Navigation("Ingredients"); + + b.Navigation("MenuCategories"); + + b.Navigation("MenuItems"); + + b.Navigation("Orders"); + + b.Navigation("Reviews"); + + b.Navigation("SubscriptionPayments"); + + b.Navigation("Tables"); + + b.Navigation("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Navigation("Attendances"); + + b.Navigation("LeaveRequests"); + + b.Navigation("Orders"); + + b.Navigation("Salaries"); + + b.Navigation("Schedules"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Navigation("Movements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Navigation("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Navigation("BranchOverrides"); + + b.Navigation("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Navigation("Items"); + + b.Navigation("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Navigation("MenuCategories"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260522044043_BranchTableOwnership.cs b/src/Meezi.Infrastructure/Data/Migrations/20260522044043_BranchTableOwnership.cs new file mode 100644 index 0000000..c6d8278 --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260522044043_BranchTableOwnership.cs @@ -0,0 +1,171 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + /// + public partial class BranchTableOwnership : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "SectionId", + table: "Tables", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "SortOrder", + table: "Tables", + type: "integer", + nullable: false, + defaultValue: 0); + + migrationBuilder.Sql( + """ + UPDATE "Tables" t + SET "BranchId" = ( + SELECT b."Id" + FROM "Branches" b + WHERE b."CafeId" = t."CafeId" + ORDER BY b."CreatedAt" + LIMIT 1 + ) + WHERE t."BranchId" IS NULL OR t."BranchId" = ''; + """); + + migrationBuilder.DropForeignKey( + name: "FK_Tables_Branches_BranchId", + table: "Tables"); + + migrationBuilder.DropIndex( + name: "IX_Tables_BranchId", + table: "Tables"); + + migrationBuilder.AlterColumn( + name: "BranchId", + table: "Tables", + type: "text", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.CreateTable( + name: "TableSections", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + BranchId = table.Column(type: "text", nullable: false), + Name = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + SortOrder = table.Column(type: "integer", nullable: false), + IsActive = table.Column(type: "boolean", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CafeId = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_TableSections", x => x.Id); + table.ForeignKey( + name: "FK_TableSections_Branches_BranchId", + column: x => x.BranchId, + principalTable: "Branches", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Tables_BranchId_SectionId_SortOrder", + table: "Tables", + columns: new[] { "BranchId", "SectionId", "SortOrder" }); + + migrationBuilder.CreateIndex( + name: "IX_Tables_SectionId", + table: "Tables", + column: "SectionId"); + + migrationBuilder.CreateIndex( + name: "IX_TableSections_BranchId_Name", + table: "TableSections", + columns: new[] { "BranchId", "Name" }); + + migrationBuilder.CreateIndex( + name: "IX_TableSections_CafeId", + table: "TableSections", + column: "CafeId"); + + migrationBuilder.AddForeignKey( + name: "FK_Tables_Branches_BranchId", + table: "Tables", + column: "BranchId", + principalTable: "Branches", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + + migrationBuilder.AddForeignKey( + name: "FK_Tables_TableSections_SectionId", + table: "Tables", + column: "SectionId", + principalTable: "TableSections", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Tables_Branches_BranchId", + table: "Tables"); + + migrationBuilder.DropForeignKey( + name: "FK_Tables_TableSections_SectionId", + table: "Tables"); + + migrationBuilder.DropTable( + name: "TableSections"); + + migrationBuilder.DropIndex( + name: "IX_Tables_BranchId_SectionId_SortOrder", + table: "Tables"); + + migrationBuilder.DropIndex( + name: "IX_Tables_SectionId", + table: "Tables"); + + migrationBuilder.DropColumn( + name: "SectionId", + table: "Tables"); + + migrationBuilder.DropColumn( + name: "SortOrder", + table: "Tables"); + + migrationBuilder.AlterColumn( + name: "BranchId", + table: "Tables", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.CreateIndex( + name: "IX_Tables_BranchId", + table: "Tables", + column: "BranchId"); + + migrationBuilder.AddForeignKey( + name: "FK_Tables_Branches_BranchId", + table: "Tables", + column: "BranchId", + principalTable: "Branches", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260522054131_AddPosDeviceSettings.Designer.cs b/src/Meezi.Infrastructure/Data/Migrations/20260522054131_AddPosDeviceSettings.Designer.cs new file mode 100644 index 0000000..de55365 --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260522054131_AddPosDeviceSettings.Designer.cs @@ -0,0 +1,2028 @@ +// +using System; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260522054131_AddPosDeviceSettings")] + partial class AddPosDeviceSettings + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ClockIn") + .HasColumnType("timestamp with time zone"); + + b.Property("ClockOut") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "Date") + .IsUnique(); + + b.ToTable("Attendances"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("AutoCutEnabled") + .HasColumnType("boolean"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("City") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("KitchenPrinterIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("KitchenPrinterPort") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("PaperWidthMm") + .HasColumnType("integer"); + + b.Property("Phone") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("PosDeviceIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("PosDevicePort") + .HasColumnType("integer"); + + b.Property("ReceiptFooter") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptHeader") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptPrinterIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("ReceiptPrinterPort") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WifiPassword") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "IsActive"); + + b.ToTable("Branches"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.BranchMenuItemOverride", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("PriceOverride") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("SortOrderOverride") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedByUserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("BranchId", "MenuItemId") + .IsUnique(); + + b.ToTable("BranchMenuItemOverrides"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("CoverImageUrl") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsVerified") + .HasColumnType("boolean"); + + b.Property("LogoUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("PlanExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("PreferredLanguage") + .IsRequired() + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SnappfoodVendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Cafes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AuthorName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("AuthorPhone") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Comment") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerRepliedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerReply") + .HasColumnType("text"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "CreatedAt"); + + b.ToTable("CafeReviews"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CashTransaction", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("ReferenceId") + .HasColumnType("text"); + + b.Property("ShiftId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("ShiftId"); + + b.HasIndex("CafeId", "BranchId"); + + b.ToTable("CashTransactions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MaxDiscount") + .HasColumnType("numeric"); + + b.Property("MinOrderAmount") + .HasColumnType("numeric"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TargetGroup") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UsageLimit") + .HasColumnType("integer"); + + b.Property("UsedCount") + .HasColumnType("integer"); + + b.Property("Value") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Code") + .IsUnique(); + + b.ToTable("Coupons"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BirthDateJalali") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Group") + .HasColumnType("integer"); + + b.Property("LoyaltyPoints") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReferredBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DailyReport", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AvgOrderValue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CardRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CashRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreditRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GeneratedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("NetIncome") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TopProducts") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("TotalExpenses") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TotalOrders") + .HasColumnType("integer"); + + b.Property("TotalRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TotalVoids") + .HasColumnType("integer"); + + b.Property("VoidAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "BranchId", "Date") + .IsUnique(); + + b.ToTable("DailyReports"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("PinCode") + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Employees"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Deductions") + .HasColumnType("numeric"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsPaid") + .HasColumnType("boolean"); + + b.Property("MonthYear") + .IsRequired() + .HasColumnType("text"); + + b.Property("NetSalary") + .HasColumnType("numeric"); + + b.Property("OvertimePay") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "MonthYear") + .IsUnique(); + + b.ToTable("EmployeeSalaries"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSchedule", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DayOfWeek") + .HasColumnType("integer"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShiftType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "DayOfWeek") + .IsUnique(); + + b.ToTable("EmployeeSchedules", (string)null); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Expense", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Category") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptImageUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ShiftId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("ShiftId"); + + b.HasIndex("CafeId", "BranchId", "CreatedAt"); + + b.ToTable("Expenses"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("QuantityOnHand") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("ReorderLevel") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("Unit") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Ingredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndDate") + .HasColumnType("date"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("ReviewedBy") + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId"); + + b.ToTable("LeaveRequests"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("TaxId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("TaxId"); + + b.ToTable("MenuCategories"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CategoryId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("CategoryId"); + + b.ToTable("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CouponId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("EmployeeId") + .HasColumnType("text"); + + b.Property("GuestName") + .HasColumnType("text"); + + b.Property("GuestPhone") + .HasColumnType("text"); + + b.Property("OrderType") + .HasColumnType("integer"); + + b.Property("ReservationId") + .HasColumnType("text"); + + b.Property("SnappfoodOrderId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subtotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("TaxTotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Total") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId"); + + b.HasIndex("CouponId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("ReservationId"); + + b.HasIndex("TableId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsVoided") + .HasColumnType("boolean"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UnitPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VoidedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VoidedByUserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Reference") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.QueueTicket", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerLabel") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedByUserId") + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("integer"); + + b.Property("OrderId") + .HasColumnType("text"); + + b.Property("ServiceDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("OrderId"); + + b.HasIndex("CafeId", "BranchId", "ServiceDate", "Number") + .IsUnique(); + + b.ToTable("QueueTickets"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClosedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ClosedByUserId") + .HasColumnType("text"); + + b.Property("ClosingCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Discrepancy") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ExpectedCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("OpenedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OpenedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("OpeningCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("ClosedByUserId"); + + b.HasIndex("OpenedByUserId"); + + b.HasIndex("BranchId", "Status"); + + b.ToTable("RegisterShifts", (string)null); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Delta") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("IngredientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Note") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("IngredientId"); + + b.ToTable("StockMovements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AmountRials") + .HasColumnType("bigint"); + + b.Property("AmountToman") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Authority") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Months") + .HasColumnType("integer"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("RefId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Authority"); + + b.HasIndex("CafeId"); + + b.ToTable("SubscriptionPayments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Floor") + .HasColumnType("text"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsCleaning") + .HasColumnType("boolean"); + + b.Property("Number") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("QrCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("SectionId") + .HasColumnType("text"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("QrCode") + .IsUnique(); + + b.HasIndex("SectionId"); + + b.HasIndex("BranchId", "SectionId", "SortOrder"); + + b.ToTable("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GuestName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GuestPhone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("PartySize") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("time without time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("TableId"); + + b.HasIndex("CafeId", "Date", "Time"); + + b.ToTable("TableReservations"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("BranchId", "Name"); + + b.ToTable("TableSections"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsCompound") + .HasColumnType("boolean"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("IsRequired") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Attendances") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Branches") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.BranchMenuItemOverride", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("BranchOverrides") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("MenuItem"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Reviews") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CashTransaction", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Shift", "Shift") + .WithMany("Transactions") + .HasForeignKey("ShiftId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Shift"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Coupons") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Customers") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DailyReport", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Staff") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Employees") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Salaries") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSchedule", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Schedules") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Expense", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Shift", "Shift") + .WithMany() + .HasForeignKey("ShiftId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Shift"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Ingredients") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("LeaveRequests") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuCategories") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Tax", "Tax") + .WithMany("MenuCategories") + .HasForeignKey("TaxId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Tax"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuItems") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuCategory", "Category") + .WithMany("MenuItems") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Orders") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Orders") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Coupon", "Coupon") + .WithMany("Orders") + .HasForeignKey("CouponId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Orders") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.TableReservation", "Reservation") + .WithMany() + .HasForeignKey("ReservationId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany("Orders") + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Coupon"); + + b.Navigation("Customer"); + + b.Navigation("Employee"); + + b.Navigation("Reservation"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("OrderItems") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Items") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MenuItem"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Payments") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.QueueTicket", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Employee", "ClosedBy") + .WithMany() + .HasForeignKey("ClosedByUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "OpenedBy") + .WithMany() + .HasForeignKey("OpenedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("ClosedBy"); + + b.Navigation("OpenedBy"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Ingredient", "Ingredient") + .WithMany("Movements") + .HasForeignKey("IngredientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Ingredient"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("SubscriptionPayments") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Tables") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Tables") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.TableSection", "Section") + .WithMany("Tables") + .HasForeignKey("SectionId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Section"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany() + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Customer"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Sections") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Taxes") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Navigation("Orders"); + + b.Navigation("Sections"); + + b.Navigation("Staff"); + + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Navigation("Branches"); + + b.Navigation("Coupons"); + + b.Navigation("Customers"); + + b.Navigation("Employees"); + + b.Navigation("Ingredients"); + + b.Navigation("MenuCategories"); + + b.Navigation("MenuItems"); + + b.Navigation("Orders"); + + b.Navigation("Reviews"); + + b.Navigation("SubscriptionPayments"); + + b.Navigation("Tables"); + + b.Navigation("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Navigation("Attendances"); + + b.Navigation("LeaveRequests"); + + b.Navigation("Orders"); + + b.Navigation("Salaries"); + + b.Navigation("Schedules"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Navigation("Movements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Navigation("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Navigation("BranchOverrides"); + + b.Navigation("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Navigation("Items"); + + b.Navigation("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Navigation("MenuCategories"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260522054131_AddPosDeviceSettings.cs b/src/Meezi.Infrastructure/Data/Migrations/20260522054131_AddPosDeviceSettings.cs new file mode 100644 index 0000000..e78ca43 --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260522054131_AddPosDeviceSettings.cs @@ -0,0 +1,39 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + /// + public partial class AddPosDeviceSettings : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "PosDeviceIp", + table: "Branches", + type: "character varying(45)", + maxLength: 45, + nullable: true); + + migrationBuilder.AddColumn( + name: "PosDevicePort", + table: "Branches", + type: "integer", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "PosDeviceIp", + table: "Branches"); + + migrationBuilder.DropColumn( + name: "PosDevicePort", + table: "Branches"); + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260522055854_FixOrderItemVoidColumns.Designer.cs b/src/Meezi.Infrastructure/Data/Migrations/20260522055854_FixOrderItemVoidColumns.Designer.cs new file mode 100644 index 0000000..278d388 --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260522055854_FixOrderItemVoidColumns.Designer.cs @@ -0,0 +1,2028 @@ +// +using System; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260522055854_FixOrderItemVoidColumns")] + partial class FixOrderItemVoidColumns + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ClockIn") + .HasColumnType("timestamp with time zone"); + + b.Property("ClockOut") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "Date") + .IsUnique(); + + b.ToTable("Attendances"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("AutoCutEnabled") + .HasColumnType("boolean"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("City") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("KitchenPrinterIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("KitchenPrinterPort") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("PaperWidthMm") + .HasColumnType("integer"); + + b.Property("Phone") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("PosDeviceIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("PosDevicePort") + .HasColumnType("integer"); + + b.Property("ReceiptFooter") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptHeader") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptPrinterIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("ReceiptPrinterPort") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WifiPassword") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "IsActive"); + + b.ToTable("Branches"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.BranchMenuItemOverride", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("PriceOverride") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("SortOrderOverride") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedByUserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("BranchId", "MenuItemId") + .IsUnique(); + + b.ToTable("BranchMenuItemOverrides"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("CoverImageUrl") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsVerified") + .HasColumnType("boolean"); + + b.Property("LogoUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("PlanExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("PreferredLanguage") + .IsRequired() + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SnappfoodVendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Cafes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AuthorName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("AuthorPhone") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Comment") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerRepliedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerReply") + .HasColumnType("text"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "CreatedAt"); + + b.ToTable("CafeReviews"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CashTransaction", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("ReferenceId") + .HasColumnType("text"); + + b.Property("ShiftId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("ShiftId"); + + b.HasIndex("CafeId", "BranchId"); + + b.ToTable("CashTransactions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MaxDiscount") + .HasColumnType("numeric"); + + b.Property("MinOrderAmount") + .HasColumnType("numeric"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TargetGroup") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UsageLimit") + .HasColumnType("integer"); + + b.Property("UsedCount") + .HasColumnType("integer"); + + b.Property("Value") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Code") + .IsUnique(); + + b.ToTable("Coupons"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BirthDateJalali") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Group") + .HasColumnType("integer"); + + b.Property("LoyaltyPoints") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReferredBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DailyReport", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AvgOrderValue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CardRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CashRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreditRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GeneratedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("NetIncome") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TopProducts") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("TotalExpenses") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TotalOrders") + .HasColumnType("integer"); + + b.Property("TotalRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TotalVoids") + .HasColumnType("integer"); + + b.Property("VoidAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "BranchId", "Date") + .IsUnique(); + + b.ToTable("DailyReports"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("PinCode") + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Employees"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Deductions") + .HasColumnType("numeric"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsPaid") + .HasColumnType("boolean"); + + b.Property("MonthYear") + .IsRequired() + .HasColumnType("text"); + + b.Property("NetSalary") + .HasColumnType("numeric"); + + b.Property("OvertimePay") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "MonthYear") + .IsUnique(); + + b.ToTable("EmployeeSalaries"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSchedule", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DayOfWeek") + .HasColumnType("integer"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShiftType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "DayOfWeek") + .IsUnique(); + + b.ToTable("EmployeeSchedules", (string)null); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Expense", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Category") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptImageUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ShiftId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("ShiftId"); + + b.HasIndex("CafeId", "BranchId", "CreatedAt"); + + b.ToTable("Expenses"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("QuantityOnHand") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("ReorderLevel") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("Unit") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Ingredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndDate") + .HasColumnType("date"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("ReviewedBy") + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId"); + + b.ToTable("LeaveRequests"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("TaxId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("TaxId"); + + b.ToTable("MenuCategories"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CategoryId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("CategoryId"); + + b.ToTable("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CouponId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("EmployeeId") + .HasColumnType("text"); + + b.Property("GuestName") + .HasColumnType("text"); + + b.Property("GuestPhone") + .HasColumnType("text"); + + b.Property("OrderType") + .HasColumnType("integer"); + + b.Property("ReservationId") + .HasColumnType("text"); + + b.Property("SnappfoodOrderId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subtotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("TaxTotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Total") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId"); + + b.HasIndex("CouponId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("ReservationId"); + + b.HasIndex("TableId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsVoided") + .HasColumnType("boolean"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UnitPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VoidedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VoidedByUserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Reference") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.QueueTicket", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerLabel") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedByUserId") + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("integer"); + + b.Property("OrderId") + .HasColumnType("text"); + + b.Property("ServiceDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("OrderId"); + + b.HasIndex("CafeId", "BranchId", "ServiceDate", "Number") + .IsUnique(); + + b.ToTable("QueueTickets"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClosedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ClosedByUserId") + .HasColumnType("text"); + + b.Property("ClosingCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Discrepancy") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ExpectedCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("OpenedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OpenedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("OpeningCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("ClosedByUserId"); + + b.HasIndex("OpenedByUserId"); + + b.HasIndex("BranchId", "Status"); + + b.ToTable("RegisterShifts", (string)null); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Delta") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("IngredientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Note") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("IngredientId"); + + b.ToTable("StockMovements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AmountRials") + .HasColumnType("bigint"); + + b.Property("AmountToman") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Authority") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Months") + .HasColumnType("integer"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("RefId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Authority"); + + b.HasIndex("CafeId"); + + b.ToTable("SubscriptionPayments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Floor") + .HasColumnType("text"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsCleaning") + .HasColumnType("boolean"); + + b.Property("Number") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("QrCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("SectionId") + .HasColumnType("text"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("QrCode") + .IsUnique(); + + b.HasIndex("SectionId"); + + b.HasIndex("BranchId", "SectionId", "SortOrder"); + + b.ToTable("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GuestName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GuestPhone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("PartySize") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("time without time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("TableId"); + + b.HasIndex("CafeId", "Date", "Time"); + + b.ToTable("TableReservations"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("BranchId", "Name"); + + b.ToTable("TableSections"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsCompound") + .HasColumnType("boolean"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("IsRequired") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Attendances") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Branches") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.BranchMenuItemOverride", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("BranchOverrides") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("MenuItem"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Reviews") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CashTransaction", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Shift", "Shift") + .WithMany("Transactions") + .HasForeignKey("ShiftId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Shift"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Coupons") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Customers") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DailyReport", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Staff") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Employees") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Salaries") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSchedule", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Schedules") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Expense", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Shift", "Shift") + .WithMany() + .HasForeignKey("ShiftId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Shift"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Ingredients") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("LeaveRequests") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuCategories") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Tax", "Tax") + .WithMany("MenuCategories") + .HasForeignKey("TaxId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Tax"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuItems") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuCategory", "Category") + .WithMany("MenuItems") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Orders") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Orders") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Coupon", "Coupon") + .WithMany("Orders") + .HasForeignKey("CouponId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Orders") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.TableReservation", "Reservation") + .WithMany() + .HasForeignKey("ReservationId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany("Orders") + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Coupon"); + + b.Navigation("Customer"); + + b.Navigation("Employee"); + + b.Navigation("Reservation"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("OrderItems") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Items") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MenuItem"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Payments") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.QueueTicket", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Employee", "ClosedBy") + .WithMany() + .HasForeignKey("ClosedByUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "OpenedBy") + .WithMany() + .HasForeignKey("OpenedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("ClosedBy"); + + b.Navigation("OpenedBy"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Ingredient", "Ingredient") + .WithMany("Movements") + .HasForeignKey("IngredientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Ingredient"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("SubscriptionPayments") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Tables") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Tables") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.TableSection", "Section") + .WithMany("Tables") + .HasForeignKey("SectionId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Section"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany() + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Customer"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Sections") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Taxes") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Navigation("Orders"); + + b.Navigation("Sections"); + + b.Navigation("Staff"); + + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Navigation("Branches"); + + b.Navigation("Coupons"); + + b.Navigation("Customers"); + + b.Navigation("Employees"); + + b.Navigation("Ingredients"); + + b.Navigation("MenuCategories"); + + b.Navigation("MenuItems"); + + b.Navigation("Orders"); + + b.Navigation("Reviews"); + + b.Navigation("SubscriptionPayments"); + + b.Navigation("Tables"); + + b.Navigation("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Navigation("Attendances"); + + b.Navigation("LeaveRequests"); + + b.Navigation("Orders"); + + b.Navigation("Salaries"); + + b.Navigation("Schedules"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Navigation("Movements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Navigation("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Navigation("BranchOverrides"); + + b.Navigation("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Navigation("Items"); + + b.Navigation("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Navigation("MenuCategories"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260522055854_FixOrderItemVoidColumns.cs b/src/Meezi.Infrastructure/Data/Migrations/20260522055854_FixOrderItemVoidColumns.cs new file mode 100644 index 0000000..d67259b --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260522055854_FixOrderItemVoidColumns.cs @@ -0,0 +1,33 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + /// + public partial class FixOrderItemVoidColumns : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + // VoidOrderLine (20260521134834) was recorded but its Up() was empty on some databases. + migrationBuilder.Sql( + """ + ALTER TABLE "OrderItems" ADD COLUMN IF NOT EXISTS "IsVoided" boolean NOT NULL DEFAULT false; + ALTER TABLE "OrderItems" ADD COLUMN IF NOT EXISTS "VoidedAt" timestamp with time zone; + ALTER TABLE "OrderItems" ADD COLUMN IF NOT EXISTS "VoidedByUserId" text; + """); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql( + """ + ALTER TABLE "OrderItems" DROP COLUMN IF EXISTS "VoidedByUserId"; + ALTER TABLE "OrderItems" DROP COLUMN IF EXISTS "VoidedAt"; + ALTER TABLE "OrderItems" DROP COLUMN IF EXISTS "IsVoided"; + """); + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260522060607_AddMenuCategoryIconAndImage.Designer.cs b/src/Meezi.Infrastructure/Data/Migrations/20260522060607_AddMenuCategoryIconAndImage.Designer.cs new file mode 100644 index 0000000..90b4ef3 --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260522060607_AddMenuCategoryIconAndImage.Designer.cs @@ -0,0 +1,2036 @@ +// +using System; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260522060607_AddMenuCategoryIconAndImage")] + partial class AddMenuCategoryIconAndImage + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ClockIn") + .HasColumnType("timestamp with time zone"); + + b.Property("ClockOut") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "Date") + .IsUnique(); + + b.ToTable("Attendances"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("AutoCutEnabled") + .HasColumnType("boolean"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("City") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("KitchenPrinterIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("KitchenPrinterPort") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("PaperWidthMm") + .HasColumnType("integer"); + + b.Property("Phone") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("PosDeviceIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("PosDevicePort") + .HasColumnType("integer"); + + b.Property("ReceiptFooter") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptHeader") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptPrinterIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("ReceiptPrinterPort") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WifiPassword") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "IsActive"); + + b.ToTable("Branches"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.BranchMenuItemOverride", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("PriceOverride") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("SortOrderOverride") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedByUserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("BranchId", "MenuItemId") + .IsUnique(); + + b.ToTable("BranchMenuItemOverrides"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("CoverImageUrl") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsVerified") + .HasColumnType("boolean"); + + b.Property("LogoUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("PlanExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("PreferredLanguage") + .IsRequired() + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SnappfoodVendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Cafes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AuthorName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("AuthorPhone") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Comment") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerRepliedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerReply") + .HasColumnType("text"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "CreatedAt"); + + b.ToTable("CafeReviews"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CashTransaction", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("ReferenceId") + .HasColumnType("text"); + + b.Property("ShiftId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("ShiftId"); + + b.HasIndex("CafeId", "BranchId"); + + b.ToTable("CashTransactions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MaxDiscount") + .HasColumnType("numeric"); + + b.Property("MinOrderAmount") + .HasColumnType("numeric"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TargetGroup") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UsageLimit") + .HasColumnType("integer"); + + b.Property("UsedCount") + .HasColumnType("integer"); + + b.Property("Value") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Code") + .IsUnique(); + + b.ToTable("Coupons"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BirthDateJalali") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Group") + .HasColumnType("integer"); + + b.Property("LoyaltyPoints") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReferredBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DailyReport", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AvgOrderValue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CardRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CashRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreditRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GeneratedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("NetIncome") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TopProducts") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("TotalExpenses") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TotalOrders") + .HasColumnType("integer"); + + b.Property("TotalRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TotalVoids") + .HasColumnType("integer"); + + b.Property("VoidAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "BranchId", "Date") + .IsUnique(); + + b.ToTable("DailyReports"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("PinCode") + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Employees"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Deductions") + .HasColumnType("numeric"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsPaid") + .HasColumnType("boolean"); + + b.Property("MonthYear") + .IsRequired() + .HasColumnType("text"); + + b.Property("NetSalary") + .HasColumnType("numeric"); + + b.Property("OvertimePay") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "MonthYear") + .IsUnique(); + + b.ToTable("EmployeeSalaries"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSchedule", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DayOfWeek") + .HasColumnType("integer"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShiftType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "DayOfWeek") + .IsUnique(); + + b.ToTable("EmployeeSchedules", (string)null); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Expense", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Category") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptImageUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ShiftId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("ShiftId"); + + b.HasIndex("CafeId", "BranchId", "CreatedAt"); + + b.ToTable("Expenses"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("QuantityOnHand") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("ReorderLevel") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("Unit") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Ingredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndDate") + .HasColumnType("date"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("ReviewedBy") + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId"); + + b.ToTable("LeaveRequests"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("Icon") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("ImageUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("TaxId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("TaxId"); + + b.ToTable("MenuCategories"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CategoryId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("CategoryId"); + + b.ToTable("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CouponId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("EmployeeId") + .HasColumnType("text"); + + b.Property("GuestName") + .HasColumnType("text"); + + b.Property("GuestPhone") + .HasColumnType("text"); + + b.Property("OrderType") + .HasColumnType("integer"); + + b.Property("ReservationId") + .HasColumnType("text"); + + b.Property("SnappfoodOrderId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subtotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("TaxTotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Total") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId"); + + b.HasIndex("CouponId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("ReservationId"); + + b.HasIndex("TableId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsVoided") + .HasColumnType("boolean"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UnitPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VoidedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VoidedByUserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Reference") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.QueueTicket", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerLabel") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedByUserId") + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("integer"); + + b.Property("OrderId") + .HasColumnType("text"); + + b.Property("ServiceDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("OrderId"); + + b.HasIndex("CafeId", "BranchId", "ServiceDate", "Number") + .IsUnique(); + + b.ToTable("QueueTickets"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClosedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ClosedByUserId") + .HasColumnType("text"); + + b.Property("ClosingCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Discrepancy") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ExpectedCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("OpenedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OpenedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("OpeningCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("ClosedByUserId"); + + b.HasIndex("OpenedByUserId"); + + b.HasIndex("BranchId", "Status"); + + b.ToTable("RegisterShifts", (string)null); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Delta") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("IngredientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Note") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("IngredientId"); + + b.ToTable("StockMovements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AmountRials") + .HasColumnType("bigint"); + + b.Property("AmountToman") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Authority") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Months") + .HasColumnType("integer"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("RefId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Authority"); + + b.HasIndex("CafeId"); + + b.ToTable("SubscriptionPayments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Floor") + .HasColumnType("text"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsCleaning") + .HasColumnType("boolean"); + + b.Property("Number") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("QrCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("SectionId") + .HasColumnType("text"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("QrCode") + .IsUnique(); + + b.HasIndex("SectionId"); + + b.HasIndex("BranchId", "SectionId", "SortOrder"); + + b.ToTable("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GuestName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GuestPhone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("PartySize") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("time without time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("TableId"); + + b.HasIndex("CafeId", "Date", "Time"); + + b.ToTable("TableReservations"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("BranchId", "Name"); + + b.ToTable("TableSections"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsCompound") + .HasColumnType("boolean"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("IsRequired") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Attendances") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Branches") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.BranchMenuItemOverride", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("BranchOverrides") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("MenuItem"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Reviews") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CashTransaction", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Shift", "Shift") + .WithMany("Transactions") + .HasForeignKey("ShiftId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Shift"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Coupons") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Customers") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DailyReport", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Staff") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Employees") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Salaries") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSchedule", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Schedules") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Expense", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Shift", "Shift") + .WithMany() + .HasForeignKey("ShiftId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Shift"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Ingredients") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("LeaveRequests") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuCategories") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Tax", "Tax") + .WithMany("MenuCategories") + .HasForeignKey("TaxId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Tax"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuItems") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuCategory", "Category") + .WithMany("MenuItems") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Orders") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Orders") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Coupon", "Coupon") + .WithMany("Orders") + .HasForeignKey("CouponId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Orders") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.TableReservation", "Reservation") + .WithMany() + .HasForeignKey("ReservationId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany("Orders") + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Coupon"); + + b.Navigation("Customer"); + + b.Navigation("Employee"); + + b.Navigation("Reservation"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("OrderItems") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Items") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MenuItem"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Payments") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.QueueTicket", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Employee", "ClosedBy") + .WithMany() + .HasForeignKey("ClosedByUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "OpenedBy") + .WithMany() + .HasForeignKey("OpenedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("ClosedBy"); + + b.Navigation("OpenedBy"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Ingredient", "Ingredient") + .WithMany("Movements") + .HasForeignKey("IngredientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Ingredient"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("SubscriptionPayments") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Tables") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Tables") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.TableSection", "Section") + .WithMany("Tables") + .HasForeignKey("SectionId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Section"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany() + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Customer"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Sections") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Taxes") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Navigation("Orders"); + + b.Navigation("Sections"); + + b.Navigation("Staff"); + + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Navigation("Branches"); + + b.Navigation("Coupons"); + + b.Navigation("Customers"); + + b.Navigation("Employees"); + + b.Navigation("Ingredients"); + + b.Navigation("MenuCategories"); + + b.Navigation("MenuItems"); + + b.Navigation("Orders"); + + b.Navigation("Reviews"); + + b.Navigation("SubscriptionPayments"); + + b.Navigation("Tables"); + + b.Navigation("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Navigation("Attendances"); + + b.Navigation("LeaveRequests"); + + b.Navigation("Orders"); + + b.Navigation("Salaries"); + + b.Navigation("Schedules"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Navigation("Movements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Navigation("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Navigation("BranchOverrides"); + + b.Navigation("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Navigation("Items"); + + b.Navigation("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Navigation("MenuCategories"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260522060607_AddMenuCategoryIconAndImage.cs b/src/Meezi.Infrastructure/Data/Migrations/20260522060607_AddMenuCategoryIconAndImage.cs new file mode 100644 index 0000000..7c61ecd --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260522060607_AddMenuCategoryIconAndImage.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + /// + public partial class AddMenuCategoryIconAndImage : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Icon", + table: "MenuCategories", + type: "character varying(32)", + maxLength: 32, + nullable: true); + + migrationBuilder.AddColumn( + name: "ImageUrl", + table: "MenuCategories", + type: "character varying(500)", + maxLength: 500, + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Icon", + table: "MenuCategories"); + + migrationBuilder.DropColumn( + name: "ImageUrl", + table: "MenuCategories"); + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260522084922_AddMenuCategoryIconPreset.Designer.cs b/src/Meezi.Infrastructure/Data/Migrations/20260522084922_AddMenuCategoryIconPreset.Designer.cs new file mode 100644 index 0000000..f5fdbe7 --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260522084922_AddMenuCategoryIconPreset.Designer.cs @@ -0,0 +1,2044 @@ +// +using System; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260522084922_AddMenuCategoryIconPreset")] + partial class AddMenuCategoryIconPreset + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ClockIn") + .HasColumnType("timestamp with time zone"); + + b.Property("ClockOut") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "Date") + .IsUnique(); + + b.ToTable("Attendances"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("AutoCutEnabled") + .HasColumnType("boolean"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("City") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("KitchenPrinterIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("KitchenPrinterPort") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("PaperWidthMm") + .HasColumnType("integer"); + + b.Property("Phone") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("PosDeviceIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("PosDevicePort") + .HasColumnType("integer"); + + b.Property("ReceiptFooter") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptHeader") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptPrinterIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("ReceiptPrinterPort") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WifiPassword") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "IsActive"); + + b.ToTable("Branches"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.BranchMenuItemOverride", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("PriceOverride") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("SortOrderOverride") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedByUserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("BranchId", "MenuItemId") + .IsUnique(); + + b.ToTable("BranchMenuItemOverrides"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("CoverImageUrl") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsVerified") + .HasColumnType("boolean"); + + b.Property("LogoUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("PlanExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("PreferredLanguage") + .IsRequired() + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SnappfoodVendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Cafes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AuthorName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("AuthorPhone") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Comment") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerRepliedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerReply") + .HasColumnType("text"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "CreatedAt"); + + b.ToTable("CafeReviews"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CashTransaction", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("ReferenceId") + .HasColumnType("text"); + + b.Property("ShiftId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("ShiftId"); + + b.HasIndex("CafeId", "BranchId"); + + b.ToTable("CashTransactions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MaxDiscount") + .HasColumnType("numeric"); + + b.Property("MinOrderAmount") + .HasColumnType("numeric"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TargetGroup") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UsageLimit") + .HasColumnType("integer"); + + b.Property("UsedCount") + .HasColumnType("integer"); + + b.Property("Value") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Code") + .IsUnique(); + + b.ToTable("Coupons"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BirthDateJalali") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Group") + .HasColumnType("integer"); + + b.Property("LoyaltyPoints") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReferredBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DailyReport", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AvgOrderValue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CardRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CashRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreditRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GeneratedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("NetIncome") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TopProducts") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("TotalExpenses") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TotalOrders") + .HasColumnType("integer"); + + b.Property("TotalRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TotalVoids") + .HasColumnType("integer"); + + b.Property("VoidAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "BranchId", "Date") + .IsUnique(); + + b.ToTable("DailyReports"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("PinCode") + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Employees"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Deductions") + .HasColumnType("numeric"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsPaid") + .HasColumnType("boolean"); + + b.Property("MonthYear") + .IsRequired() + .HasColumnType("text"); + + b.Property("NetSalary") + .HasColumnType("numeric"); + + b.Property("OvertimePay") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "MonthYear") + .IsUnique(); + + b.ToTable("EmployeeSalaries"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSchedule", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DayOfWeek") + .HasColumnType("integer"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShiftType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "DayOfWeek") + .IsUnique(); + + b.ToTable("EmployeeSchedules", (string)null); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Expense", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Category") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptImageUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ShiftId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("ShiftId"); + + b.HasIndex("CafeId", "BranchId", "CreatedAt"); + + b.ToTable("Expenses"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("QuantityOnHand") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("ReorderLevel") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("Unit") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Ingredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndDate") + .HasColumnType("date"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("ReviewedBy") + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId"); + + b.ToTable("LeaveRequests"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("Icon") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("IconPresetId") + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("IconStyle") + .HasMaxLength(16) + .HasColumnType("character varying(16)"); + + b.Property("ImageUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("TaxId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("TaxId"); + + b.ToTable("MenuCategories"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CategoryId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("CategoryId"); + + b.ToTable("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CouponId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("EmployeeId") + .HasColumnType("text"); + + b.Property("GuestName") + .HasColumnType("text"); + + b.Property("GuestPhone") + .HasColumnType("text"); + + b.Property("OrderType") + .HasColumnType("integer"); + + b.Property("ReservationId") + .HasColumnType("text"); + + b.Property("SnappfoodOrderId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subtotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("TaxTotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Total") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId"); + + b.HasIndex("CouponId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("ReservationId"); + + b.HasIndex("TableId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsVoided") + .HasColumnType("boolean"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UnitPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VoidedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VoidedByUserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Reference") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.QueueTicket", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerLabel") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedByUserId") + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("integer"); + + b.Property("OrderId") + .HasColumnType("text"); + + b.Property("ServiceDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("OrderId"); + + b.HasIndex("CafeId", "BranchId", "ServiceDate", "Number") + .IsUnique(); + + b.ToTable("QueueTickets"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClosedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ClosedByUserId") + .HasColumnType("text"); + + b.Property("ClosingCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Discrepancy") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ExpectedCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("OpenedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OpenedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("OpeningCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("ClosedByUserId"); + + b.HasIndex("OpenedByUserId"); + + b.HasIndex("BranchId", "Status"); + + b.ToTable("RegisterShifts", (string)null); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Delta") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("IngredientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Note") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("IngredientId"); + + b.ToTable("StockMovements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AmountRials") + .HasColumnType("bigint"); + + b.Property("AmountToman") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Authority") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Months") + .HasColumnType("integer"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("RefId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Authority"); + + b.HasIndex("CafeId"); + + b.ToTable("SubscriptionPayments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Floor") + .HasColumnType("text"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsCleaning") + .HasColumnType("boolean"); + + b.Property("Number") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("QrCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("SectionId") + .HasColumnType("text"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("QrCode") + .IsUnique(); + + b.HasIndex("SectionId"); + + b.HasIndex("BranchId", "SectionId", "SortOrder"); + + b.ToTable("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GuestName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GuestPhone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("PartySize") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("time without time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("TableId"); + + b.HasIndex("CafeId", "Date", "Time"); + + b.ToTable("TableReservations"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("BranchId", "Name"); + + b.ToTable("TableSections"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsCompound") + .HasColumnType("boolean"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("IsRequired") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Attendances") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Branches") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.BranchMenuItemOverride", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("BranchOverrides") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("MenuItem"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Reviews") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CashTransaction", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Shift", "Shift") + .WithMany("Transactions") + .HasForeignKey("ShiftId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Shift"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Coupons") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Customers") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DailyReport", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Staff") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Employees") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Salaries") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSchedule", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Schedules") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Expense", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Shift", "Shift") + .WithMany() + .HasForeignKey("ShiftId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Shift"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Ingredients") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("LeaveRequests") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuCategories") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Tax", "Tax") + .WithMany("MenuCategories") + .HasForeignKey("TaxId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Tax"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuItems") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuCategory", "Category") + .WithMany("MenuItems") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Orders") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Orders") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Coupon", "Coupon") + .WithMany("Orders") + .HasForeignKey("CouponId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Orders") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.TableReservation", "Reservation") + .WithMany() + .HasForeignKey("ReservationId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany("Orders") + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Coupon"); + + b.Navigation("Customer"); + + b.Navigation("Employee"); + + b.Navigation("Reservation"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("OrderItems") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Items") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MenuItem"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Payments") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.QueueTicket", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Employee", "ClosedBy") + .WithMany() + .HasForeignKey("ClosedByUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "OpenedBy") + .WithMany() + .HasForeignKey("OpenedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("ClosedBy"); + + b.Navigation("OpenedBy"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Ingredient", "Ingredient") + .WithMany("Movements") + .HasForeignKey("IngredientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Ingredient"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("SubscriptionPayments") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Tables") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Tables") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.TableSection", "Section") + .WithMany("Tables") + .HasForeignKey("SectionId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Section"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany() + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Customer"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Sections") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Taxes") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Navigation("Orders"); + + b.Navigation("Sections"); + + b.Navigation("Staff"); + + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Navigation("Branches"); + + b.Navigation("Coupons"); + + b.Navigation("Customers"); + + b.Navigation("Employees"); + + b.Navigation("Ingredients"); + + b.Navigation("MenuCategories"); + + b.Navigation("MenuItems"); + + b.Navigation("Orders"); + + b.Navigation("Reviews"); + + b.Navigation("SubscriptionPayments"); + + b.Navigation("Tables"); + + b.Navigation("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Navigation("Attendances"); + + b.Navigation("LeaveRequests"); + + b.Navigation("Orders"); + + b.Navigation("Salaries"); + + b.Navigation("Schedules"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Navigation("Movements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Navigation("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Navigation("BranchOverrides"); + + b.Navigation("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Navigation("Items"); + + b.Navigation("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Navigation("MenuCategories"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260522084922_AddMenuCategoryIconPreset.cs b/src/Meezi.Infrastructure/Data/Migrations/20260522084922_AddMenuCategoryIconPreset.cs new file mode 100644 index 0000000..f45a2f5 --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260522084922_AddMenuCategoryIconPreset.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + /// + public partial class AddMenuCategoryIconPreset : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "IconPresetId", + table: "MenuCategories", + type: "character varying(48)", + maxLength: 48, + nullable: true); + + migrationBuilder.AddColumn( + name: "IconStyle", + table: "MenuCategories", + type: "character varying(16)", + maxLength: 16, + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "IconPresetId", + table: "MenuCategories"); + + migrationBuilder.DropColumn( + name: "IconStyle", + table: "MenuCategories"); + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260522111039_AddCafeThemeJson.Designer.cs b/src/Meezi.Infrastructure/Data/Migrations/20260522111039_AddCafeThemeJson.Designer.cs new file mode 100644 index 0000000..c72ab07 --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260522111039_AddCafeThemeJson.Designer.cs @@ -0,0 +1,2048 @@ +// +using System; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260522111039_AddCafeThemeJson")] + partial class AddCafeThemeJson + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ClockIn") + .HasColumnType("timestamp with time zone"); + + b.Property("ClockOut") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "Date") + .IsUnique(); + + b.ToTable("Attendances"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("AutoCutEnabled") + .HasColumnType("boolean"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("City") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("KitchenPrinterIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("KitchenPrinterPort") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("PaperWidthMm") + .HasColumnType("integer"); + + b.Property("Phone") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("PosDeviceIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("PosDevicePort") + .HasColumnType("integer"); + + b.Property("ReceiptFooter") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptHeader") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptPrinterIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("ReceiptPrinterPort") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WifiPassword") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "IsActive"); + + b.ToTable("Branches"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.BranchMenuItemOverride", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("PriceOverride") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("SortOrderOverride") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedByUserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("BranchId", "MenuItemId") + .IsUnique(); + + b.ToTable("BranchMenuItemOverrides"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("CoverImageUrl") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsVerified") + .HasColumnType("boolean"); + + b.Property("LogoUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("PlanExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("PreferredLanguage") + .IsRequired() + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SnappfoodVendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ThemeJson") + .HasMaxLength(8000) + .HasColumnType("character varying(8000)"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Cafes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AuthorName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("AuthorPhone") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Comment") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerRepliedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerReply") + .HasColumnType("text"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "CreatedAt"); + + b.ToTable("CafeReviews"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CashTransaction", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("ReferenceId") + .HasColumnType("text"); + + b.Property("ShiftId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("ShiftId"); + + b.HasIndex("CafeId", "BranchId"); + + b.ToTable("CashTransactions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MaxDiscount") + .HasColumnType("numeric"); + + b.Property("MinOrderAmount") + .HasColumnType("numeric"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TargetGroup") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UsageLimit") + .HasColumnType("integer"); + + b.Property("UsedCount") + .HasColumnType("integer"); + + b.Property("Value") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Code") + .IsUnique(); + + b.ToTable("Coupons"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BirthDateJalali") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Group") + .HasColumnType("integer"); + + b.Property("LoyaltyPoints") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReferredBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DailyReport", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AvgOrderValue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CardRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CashRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreditRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GeneratedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("NetIncome") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TopProducts") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("TotalExpenses") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TotalOrders") + .HasColumnType("integer"); + + b.Property("TotalRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TotalVoids") + .HasColumnType("integer"); + + b.Property("VoidAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "BranchId", "Date") + .IsUnique(); + + b.ToTable("DailyReports"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("PinCode") + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Employees"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Deductions") + .HasColumnType("numeric"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsPaid") + .HasColumnType("boolean"); + + b.Property("MonthYear") + .IsRequired() + .HasColumnType("text"); + + b.Property("NetSalary") + .HasColumnType("numeric"); + + b.Property("OvertimePay") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "MonthYear") + .IsUnique(); + + b.ToTable("EmployeeSalaries"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSchedule", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DayOfWeek") + .HasColumnType("integer"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShiftType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "DayOfWeek") + .IsUnique(); + + b.ToTable("EmployeeSchedules", (string)null); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Expense", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Category") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptImageUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ShiftId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("ShiftId"); + + b.HasIndex("CafeId", "BranchId", "CreatedAt"); + + b.ToTable("Expenses"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("QuantityOnHand") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("ReorderLevel") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("Unit") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Ingredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndDate") + .HasColumnType("date"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("ReviewedBy") + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId"); + + b.ToTable("LeaveRequests"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("Icon") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("IconPresetId") + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("IconStyle") + .HasMaxLength(16) + .HasColumnType("character varying(16)"); + + b.Property("ImageUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("TaxId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("TaxId"); + + b.ToTable("MenuCategories"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CategoryId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("CategoryId"); + + b.ToTable("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CouponId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("EmployeeId") + .HasColumnType("text"); + + b.Property("GuestName") + .HasColumnType("text"); + + b.Property("GuestPhone") + .HasColumnType("text"); + + b.Property("OrderType") + .HasColumnType("integer"); + + b.Property("ReservationId") + .HasColumnType("text"); + + b.Property("SnappfoodOrderId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subtotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("TaxTotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Total") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId"); + + b.HasIndex("CouponId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("ReservationId"); + + b.HasIndex("TableId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsVoided") + .HasColumnType("boolean"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UnitPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VoidedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VoidedByUserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Reference") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.QueueTicket", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerLabel") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedByUserId") + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("integer"); + + b.Property("OrderId") + .HasColumnType("text"); + + b.Property("ServiceDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("OrderId"); + + b.HasIndex("CafeId", "BranchId", "ServiceDate", "Number") + .IsUnique(); + + b.ToTable("QueueTickets"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClosedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ClosedByUserId") + .HasColumnType("text"); + + b.Property("ClosingCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Discrepancy") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ExpectedCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("OpenedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OpenedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("OpeningCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("ClosedByUserId"); + + b.HasIndex("OpenedByUserId"); + + b.HasIndex("BranchId", "Status"); + + b.ToTable("RegisterShifts", (string)null); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Delta") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("IngredientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Note") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("IngredientId"); + + b.ToTable("StockMovements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AmountRials") + .HasColumnType("bigint"); + + b.Property("AmountToman") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Authority") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Months") + .HasColumnType("integer"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("RefId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Authority"); + + b.HasIndex("CafeId"); + + b.ToTable("SubscriptionPayments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Floor") + .HasColumnType("text"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsCleaning") + .HasColumnType("boolean"); + + b.Property("Number") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("QrCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("SectionId") + .HasColumnType("text"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("QrCode") + .IsUnique(); + + b.HasIndex("SectionId"); + + b.HasIndex("BranchId", "SectionId", "SortOrder"); + + b.ToTable("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GuestName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GuestPhone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("PartySize") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("time without time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("TableId"); + + b.HasIndex("CafeId", "Date", "Time"); + + b.ToTable("TableReservations"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("BranchId", "Name"); + + b.ToTable("TableSections"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsCompound") + .HasColumnType("boolean"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("IsRequired") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Attendances") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Branches") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.BranchMenuItemOverride", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("BranchOverrides") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("MenuItem"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Reviews") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CashTransaction", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Shift", "Shift") + .WithMany("Transactions") + .HasForeignKey("ShiftId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Shift"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Coupons") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Customers") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DailyReport", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Staff") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Employees") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Salaries") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSchedule", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Schedules") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Expense", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Shift", "Shift") + .WithMany() + .HasForeignKey("ShiftId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Shift"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Ingredients") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("LeaveRequests") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuCategories") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Tax", "Tax") + .WithMany("MenuCategories") + .HasForeignKey("TaxId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Tax"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuItems") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuCategory", "Category") + .WithMany("MenuItems") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Orders") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Orders") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Coupon", "Coupon") + .WithMany("Orders") + .HasForeignKey("CouponId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Orders") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.TableReservation", "Reservation") + .WithMany() + .HasForeignKey("ReservationId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany("Orders") + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Coupon"); + + b.Navigation("Customer"); + + b.Navigation("Employee"); + + b.Navigation("Reservation"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("OrderItems") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Items") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MenuItem"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Payments") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.QueueTicket", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Employee", "ClosedBy") + .WithMany() + .HasForeignKey("ClosedByUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "OpenedBy") + .WithMany() + .HasForeignKey("OpenedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("ClosedBy"); + + b.Navigation("OpenedBy"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Ingredient", "Ingredient") + .WithMany("Movements") + .HasForeignKey("IngredientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Ingredient"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("SubscriptionPayments") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Tables") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Tables") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.TableSection", "Section") + .WithMany("Tables") + .HasForeignKey("SectionId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Section"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany() + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Customer"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Sections") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Taxes") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Navigation("Orders"); + + b.Navigation("Sections"); + + b.Navigation("Staff"); + + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Navigation("Branches"); + + b.Navigation("Coupons"); + + b.Navigation("Customers"); + + b.Navigation("Employees"); + + b.Navigation("Ingredients"); + + b.Navigation("MenuCategories"); + + b.Navigation("MenuItems"); + + b.Navigation("Orders"); + + b.Navigation("Reviews"); + + b.Navigation("SubscriptionPayments"); + + b.Navigation("Tables"); + + b.Navigation("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Navigation("Attendances"); + + b.Navigation("LeaveRequests"); + + b.Navigation("Orders"); + + b.Navigation("Salaries"); + + b.Navigation("Schedules"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Navigation("Movements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Navigation("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Navigation("BranchOverrides"); + + b.Navigation("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Navigation("Items"); + + b.Navigation("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Navigation("MenuCategories"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260522111039_AddCafeThemeJson.cs b/src/Meezi.Infrastructure/Data/Migrations/20260522111039_AddCafeThemeJson.cs new file mode 100644 index 0000000..d13848c --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260522111039_AddCafeThemeJson.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + /// + public partial class AddCafeThemeJson : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ThemeJson", + table: "Cafes", + type: "character varying(8000)", + maxLength: 8000, + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ThemeJson", + table: "Cafes"); + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260522113654_QrGuestMenuAndBranchIdentity.Designer.cs b/src/Meezi.Infrastructure/Data/Migrations/20260522113654_QrGuestMenuAndBranchIdentity.Designer.cs new file mode 100644 index 0000000..33a2705 --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260522113654_QrGuestMenuAndBranchIdentity.Designer.cs @@ -0,0 +1,2069 @@ +// +using System; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260522113654_QrGuestMenuAndBranchIdentity")] + partial class QrGuestMenuAndBranchIdentity + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ClockIn") + .HasColumnType("timestamp with time zone"); + + b.Property("ClockOut") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "Date") + .IsUnique(); + + b.ToTable("Attendances"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccentColor") + .HasColumnType("text"); + + b.Property("Address") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("AutoCutEnabled") + .HasColumnType("boolean"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("City") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("KitchenPrinterIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("KitchenPrinterPort") + .HasColumnType("integer"); + + b.Property("LogoUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("PaperWidthMm") + .HasColumnType("integer"); + + b.Property("Phone") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("PosDeviceIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("PosDevicePort") + .HasColumnType("integer"); + + b.Property("ReceiptFooter") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptHeader") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptPrinterIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("ReceiptPrinterPort") + .HasColumnType("integer"); + + b.Property("TaxRate") + .HasColumnType("numeric"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WelcomeText") + .HasColumnType("text"); + + b.Property("WifiPassword") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "IsActive"); + + b.ToTable("Branches"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.BranchMenuItemOverride", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("PriceOverride") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("SortOrderOverride") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedByUserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("BranchId", "MenuItemId") + .IsUnique(); + + b.ToTable("BranchMenuItemOverrides"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("AllowBranchTaxOverride") + .HasColumnType("boolean"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("CoverImageUrl") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DefaultTaxRate") + .HasColumnType("numeric"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsVerified") + .HasColumnType("boolean"); + + b.Property("LogoUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("PlanExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("PreferredLanguage") + .IsRequired() + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SnappfoodVendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ThemeJson") + .HasMaxLength(8000) + .HasColumnType("character varying(8000)"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Cafes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AuthorName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("AuthorPhone") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Comment") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerRepliedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerReply") + .HasColumnType("text"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "CreatedAt"); + + b.ToTable("CafeReviews"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CashTransaction", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("ReferenceId") + .HasColumnType("text"); + + b.Property("ShiftId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("ShiftId"); + + b.HasIndex("CafeId", "BranchId"); + + b.ToTable("CashTransactions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MaxDiscount") + .HasColumnType("numeric"); + + b.Property("MinOrderAmount") + .HasColumnType("numeric"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TargetGroup") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UsageLimit") + .HasColumnType("integer"); + + b.Property("UsedCount") + .HasColumnType("integer"); + + b.Property("Value") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Code") + .IsUnique(); + + b.ToTable("Coupons"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BirthDateJalali") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Group") + .HasColumnType("integer"); + + b.Property("LoyaltyPoints") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReferredBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DailyReport", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AvgOrderValue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CardRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CashRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreditRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GeneratedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("NetIncome") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TopProducts") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("TotalExpenses") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TotalOrders") + .HasColumnType("integer"); + + b.Property("TotalRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TotalVoids") + .HasColumnType("integer"); + + b.Property("VoidAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "BranchId", "Date") + .IsUnique(); + + b.ToTable("DailyReports"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("PinCode") + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Employees"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Deductions") + .HasColumnType("numeric"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsPaid") + .HasColumnType("boolean"); + + b.Property("MonthYear") + .IsRequired() + .HasColumnType("text"); + + b.Property("NetSalary") + .HasColumnType("numeric"); + + b.Property("OvertimePay") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "MonthYear") + .IsUnique(); + + b.ToTable("EmployeeSalaries"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSchedule", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DayOfWeek") + .HasColumnType("integer"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShiftType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "DayOfWeek") + .IsUnique(); + + b.ToTable("EmployeeSchedules", (string)null); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Expense", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Category") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptImageUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ShiftId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("ShiftId"); + + b.HasIndex("CafeId", "BranchId", "CreatedAt"); + + b.ToTable("Expenses"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("QuantityOnHand") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("ReorderLevel") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("Unit") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Ingredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndDate") + .HasColumnType("date"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("ReviewedBy") + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId"); + + b.ToTable("LeaveRequests"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("Icon") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("IconPresetId") + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("IconStyle") + .HasMaxLength(16) + .HasColumnType("character varying(16)"); + + b.Property("ImageUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("TaxId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("TaxId"); + + b.ToTable("MenuCategories"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CategoryId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("CategoryId"); + + b.ToTable("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CouponId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("EmployeeId") + .HasColumnType("text"); + + b.Property("GuestName") + .HasColumnType("text"); + + b.Property("GuestPhone") + .HasColumnType("text"); + + b.Property("OrderType") + .HasColumnType("integer"); + + b.Property("ReservationId") + .HasColumnType("text"); + + b.Property("SnappfoodOrderId") + .HasColumnType("text"); + + b.Property("Source") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subtotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("TaxTotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Total") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId"); + + b.HasIndex("CouponId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("ReservationId"); + + b.HasIndex("TableId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsVoided") + .HasColumnType("boolean"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UnitPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VoidedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VoidedByUserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Reference") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.QueueTicket", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerLabel") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedByUserId") + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("integer"); + + b.Property("OrderId") + .HasColumnType("text"); + + b.Property("ServiceDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("OrderId"); + + b.HasIndex("CafeId", "BranchId", "ServiceDate", "Number") + .IsUnique(); + + b.ToTable("QueueTickets"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClosedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ClosedByUserId") + .HasColumnType("text"); + + b.Property("ClosingCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Discrepancy") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ExpectedCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("OpenedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OpenedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("OpeningCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("ClosedByUserId"); + + b.HasIndex("OpenedByUserId"); + + b.HasIndex("BranchId", "Status"); + + b.ToTable("RegisterShifts", (string)null); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Delta") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("IngredientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Note") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("IngredientId"); + + b.ToTable("StockMovements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AmountRials") + .HasColumnType("bigint"); + + b.Property("AmountToman") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Authority") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Months") + .HasColumnType("integer"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("RefId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Authority"); + + b.HasIndex("CafeId"); + + b.ToTable("SubscriptionPayments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Floor") + .HasColumnType("text"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsCleaning") + .HasColumnType("boolean"); + + b.Property("Number") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("QrCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("SectionId") + .HasColumnType("text"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("QrCode") + .IsUnique(); + + b.HasIndex("SectionId"); + + b.HasIndex("BranchId", "SectionId", "SortOrder"); + + b.ToTable("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GuestName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GuestPhone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("PartySize") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("time without time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("TableId"); + + b.HasIndex("CafeId", "Date", "Time"); + + b.ToTable("TableReservations"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("BranchId", "Name"); + + b.ToTable("TableSections"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsCompound") + .HasColumnType("boolean"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("IsRequired") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Attendances") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Branches") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.BranchMenuItemOverride", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("BranchOverrides") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("MenuItem"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Reviews") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CashTransaction", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Shift", "Shift") + .WithMany("Transactions") + .HasForeignKey("ShiftId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Shift"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Coupons") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Customers") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DailyReport", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Staff") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Employees") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Salaries") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSchedule", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Schedules") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Expense", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Shift", "Shift") + .WithMany() + .HasForeignKey("ShiftId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Shift"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Ingredients") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("LeaveRequests") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuCategories") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Tax", "Tax") + .WithMany("MenuCategories") + .HasForeignKey("TaxId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Tax"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuItems") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuCategory", "Category") + .WithMany("MenuItems") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Orders") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Orders") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Coupon", "Coupon") + .WithMany("Orders") + .HasForeignKey("CouponId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Orders") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.TableReservation", "Reservation") + .WithMany() + .HasForeignKey("ReservationId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany("Orders") + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Coupon"); + + b.Navigation("Customer"); + + b.Navigation("Employee"); + + b.Navigation("Reservation"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("OrderItems") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Items") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MenuItem"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Payments") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.QueueTicket", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Employee", "ClosedBy") + .WithMany() + .HasForeignKey("ClosedByUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "OpenedBy") + .WithMany() + .HasForeignKey("OpenedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("ClosedBy"); + + b.Navigation("OpenedBy"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Ingredient", "Ingredient") + .WithMany("Movements") + .HasForeignKey("IngredientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Ingredient"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("SubscriptionPayments") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Tables") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Tables") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.TableSection", "Section") + .WithMany("Tables") + .HasForeignKey("SectionId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Section"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany() + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Customer"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Sections") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Taxes") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Navigation("Orders"); + + b.Navigation("Sections"); + + b.Navigation("Staff"); + + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Navigation("Branches"); + + b.Navigation("Coupons"); + + b.Navigation("Customers"); + + b.Navigation("Employees"); + + b.Navigation("Ingredients"); + + b.Navigation("MenuCategories"); + + b.Navigation("MenuItems"); + + b.Navigation("Orders"); + + b.Navigation("Reviews"); + + b.Navigation("SubscriptionPayments"); + + b.Navigation("Tables"); + + b.Navigation("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Navigation("Attendances"); + + b.Navigation("LeaveRequests"); + + b.Navigation("Orders"); + + b.Navigation("Salaries"); + + b.Navigation("Schedules"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Navigation("Movements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Navigation("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Navigation("BranchOverrides"); + + b.Navigation("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Navigation("Items"); + + b.Navigation("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Navigation("MenuCategories"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260522113654_QrGuestMenuAndBranchIdentity.cs b/src/Meezi.Infrastructure/Data/Migrations/20260522113654_QrGuestMenuAndBranchIdentity.cs new file mode 100644 index 0000000..fadfecc --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260522113654_QrGuestMenuAndBranchIdentity.cs @@ -0,0 +1,91 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + /// + public partial class QrGuestMenuAndBranchIdentity : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Source", + table: "Orders", + type: "integer", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "AllowBranchTaxOverride", + table: "Cafes", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "DefaultTaxRate", + table: "Cafes", + type: "numeric", + nullable: false, + defaultValue: 0m); + + migrationBuilder.AddColumn( + name: "AccentColor", + table: "Branches", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "LogoUrl", + table: "Branches", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "TaxRate", + table: "Branches", + type: "numeric", + nullable: true); + + migrationBuilder.AddColumn( + name: "WelcomeText", + table: "Branches", + type: "text", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Source", + table: "Orders"); + + migrationBuilder.DropColumn( + name: "AllowBranchTaxOverride", + table: "Cafes"); + + migrationBuilder.DropColumn( + name: "DefaultTaxRate", + table: "Cafes"); + + migrationBuilder.DropColumn( + name: "AccentColor", + table: "Branches"); + + migrationBuilder.DropColumn( + name: "LogoUrl", + table: "Branches"); + + migrationBuilder.DropColumn( + name: "TaxRate", + table: "Branches"); + + migrationBuilder.DropColumn( + name: "WelcomeText", + table: "Branches"); + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260522115531_DeliveryPlatformIntegration.Designer.cs b/src/Meezi.Infrastructure/Data/Migrations/20260522115531_DeliveryPlatformIntegration.Designer.cs new file mode 100644 index 0000000..f4bfb24 --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260522115531_DeliveryPlatformIntegration.Designer.cs @@ -0,0 +1,2210 @@ +// +using System; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260522115531_DeliveryPlatformIntegration")] + partial class DeliveryPlatformIntegration + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ClockIn") + .HasColumnType("timestamp with time zone"); + + b.Property("ClockOut") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "Date") + .IsUnique(); + + b.ToTable("Attendances"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccentColor") + .HasColumnType("text"); + + b.Property("Address") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("AutoCutEnabled") + .HasColumnType("boolean"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("City") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("KitchenPrinterIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("KitchenPrinterPort") + .HasColumnType("integer"); + + b.Property("LogoUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("PaperWidthMm") + .HasColumnType("integer"); + + b.Property("Phone") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("PosDeviceIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("PosDevicePort") + .HasColumnType("integer"); + + b.Property("ReceiptFooter") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptHeader") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptPrinterIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("ReceiptPrinterPort") + .HasColumnType("integer"); + + b.Property("TaxRate") + .HasColumnType("numeric"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WelcomeText") + .HasColumnType("text"); + + b.Property("WifiPassword") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "IsActive"); + + b.ToTable("Branches"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.BranchMenuItemOverride", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("PriceOverride") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("SortOrderOverride") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedByUserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("BranchId", "MenuItemId") + .IsUnique(); + + b.ToTable("BranchMenuItemOverrides"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("AllowBranchTaxOverride") + .HasColumnType("boolean"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("CoverImageUrl") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DefaultTaxRate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DigikalaVendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("IsVerified") + .HasColumnType("boolean"); + + b.Property("LogoUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("PlanExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("PreferredLanguage") + .IsRequired() + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SnappfoodVendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Tap30VendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ThemeJson") + .HasMaxLength(8000) + .HasColumnType("character varying(8000)"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Cafes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AuthorName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("AuthorPhone") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Comment") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerRepliedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerReply") + .HasColumnType("text"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "CreatedAt"); + + b.ToTable("CafeReviews"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CashTransaction", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("ReferenceId") + .HasColumnType("text"); + + b.Property("ShiftId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("ShiftId"); + + b.HasIndex("CafeId", "BranchId"); + + b.ToTable("CashTransactions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MaxDiscount") + .HasColumnType("numeric"); + + b.Property("MinOrderAmount") + .HasColumnType("numeric"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TargetGroup") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UsageLimit") + .HasColumnType("integer"); + + b.Property("UsedCount") + .HasColumnType("integer"); + + b.Property("Value") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Code") + .IsUnique(); + + b.ToTable("Coupons"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BirthDateJalali") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Group") + .HasColumnType("integer"); + + b.Property("LoyaltyPoints") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReferredBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DailyReport", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AvgOrderValue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CardRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CashRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreditRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GeneratedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("NetIncome") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TopProducts") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("TotalExpenses") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TotalOrders") + .HasColumnType("integer"); + + b.Property("TotalRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TotalVoids") + .HasColumnType("integer"); + + b.Property("VoidAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "BranchId", "Date") + .IsUnique(); + + b.ToTable("DailyReports"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DeliveryCommissionRate", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Platform") + .HasColumnType("integer"); + + b.Property("RatePercent") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Platform") + .IsUnique(); + + b.ToTable("DeliveryCommissionRates"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("PinCode") + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Employees"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Deductions") + .HasColumnType("numeric"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsPaid") + .HasColumnType("boolean"); + + b.Property("MonthYear") + .IsRequired() + .HasColumnType("text"); + + b.Property("NetSalary") + .HasColumnType("numeric"); + + b.Property("OvertimePay") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "MonthYear") + .IsUnique(); + + b.ToTable("EmployeeSalaries"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSchedule", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DayOfWeek") + .HasColumnType("integer"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShiftType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "DayOfWeek") + .IsUnique(); + + b.ToTable("EmployeeSchedules", (string)null); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Expense", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Category") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptImageUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ShiftId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("ShiftId"); + + b.HasIndex("CafeId", "BranchId", "CreatedAt"); + + b.ToTable("Expenses"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("QuantityOnHand") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("ReorderLevel") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("Unit") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Ingredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndDate") + .HasColumnType("date"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("ReviewedBy") + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId"); + + b.ToTable("LeaveRequests"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("Icon") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("IconPresetId") + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("IconStyle") + .HasMaxLength(16) + .HasColumnType("character varying(16)"); + + b.Property("ImageUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("TaxId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("TaxId"); + + b.ToTable("MenuCategories"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CategoryId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("CategoryId"); + + b.ToTable("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CouponId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeliveryMetaJson") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("DeliveryPlatform") + .HasColumnType("integer"); + + b.Property("DiscountAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("EmployeeId") + .HasColumnType("text"); + + b.Property("ExternalOrderId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("GuestName") + .HasColumnType("text"); + + b.Property("GuestPhone") + .HasColumnType("text"); + + b.Property("OrderType") + .HasColumnType("integer"); + + b.Property("PlatformCommission") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ReservationId") + .HasColumnType("text"); + + b.Property("SnappfoodOrderId") + .HasColumnType("text"); + + b.Property("Source") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subtotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("TaxTotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Total") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CouponId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("ReservationId"); + + b.HasIndex("TableId"); + + b.HasIndex("CafeId", "DeliveryPlatform", "ExternalOrderId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsVoided") + .HasColumnType("boolean"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UnitPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VoidedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VoidedByUserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Reference") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.QueueTicket", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerLabel") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedByUserId") + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("integer"); + + b.Property("OrderId") + .HasColumnType("text"); + + b.Property("ServiceDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("OrderId"); + + b.HasIndex("CafeId", "BranchId", "ServiceDate", "Number") + .IsUnique(); + + b.ToTable("QueueTickets"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClosedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ClosedByUserId") + .HasColumnType("text"); + + b.Property("ClosingCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Discrepancy") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ExpectedCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("OpenedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OpenedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("OpeningCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("ClosedByUserId"); + + b.HasIndex("OpenedByUserId"); + + b.HasIndex("BranchId", "Status"); + + b.ToTable("RegisterShifts", (string)null); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Delta") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("IngredientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Note") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("IngredientId"); + + b.ToTable("StockMovements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AmountRials") + .HasColumnType("bigint"); + + b.Property("AmountToman") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Authority") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Months") + .HasColumnType("integer"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("RefId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Authority"); + + b.HasIndex("CafeId"); + + b.ToTable("SubscriptionPayments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Floor") + .HasColumnType("text"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsCleaning") + .HasColumnType("boolean"); + + b.Property("Number") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("QrCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("SectionId") + .HasColumnType("text"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("QrCode") + .IsUnique(); + + b.HasIndex("SectionId"); + + b.HasIndex("BranchId", "SectionId", "SortOrder"); + + b.ToTable("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GuestName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GuestPhone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("PartySize") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("time without time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("TableId"); + + b.HasIndex("CafeId", "Date", "Time"); + + b.ToTable("TableReservations"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("BranchId", "Name"); + + b.ToTable("TableSections"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsCompound") + .HasColumnType("boolean"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("IsRequired") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.WebhookLog", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AttemptCount") + .HasColumnType("integer"); + + b.Property("CafeId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("ExternalOrderId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("MeeziOrderId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Platform") + .HasColumnType("integer"); + + b.Property("Processed") + .HasColumnType("boolean"); + + b.Property("ProcessedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RawBody") + .IsRequired() + .HasColumnType("text"); + + b.Property("SignatureHeader") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("SignatureValid") + .HasColumnType("boolean"); + + b.Property("Success") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("Platform", "CreatedAt"); + + b.ToTable("WebhookLogs"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Attendances") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Branches") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.BranchMenuItemOverride", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("BranchOverrides") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("MenuItem"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Reviews") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CashTransaction", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Shift", "Shift") + .WithMany("Transactions") + .HasForeignKey("ShiftId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Shift"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Coupons") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Customers") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DailyReport", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DeliveryCommissionRate", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Staff") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Employees") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Salaries") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSchedule", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Schedules") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Expense", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Shift", "Shift") + .WithMany() + .HasForeignKey("ShiftId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Shift"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Ingredients") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("LeaveRequests") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuCategories") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Tax", "Tax") + .WithMany("MenuCategories") + .HasForeignKey("TaxId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Tax"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuItems") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuCategory", "Category") + .WithMany("MenuItems") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Orders") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Orders") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Coupon", "Coupon") + .WithMany("Orders") + .HasForeignKey("CouponId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Orders") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.TableReservation", "Reservation") + .WithMany() + .HasForeignKey("ReservationId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany("Orders") + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Coupon"); + + b.Navigation("Customer"); + + b.Navigation("Employee"); + + b.Navigation("Reservation"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("OrderItems") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Items") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MenuItem"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Payments") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.QueueTicket", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Employee", "ClosedBy") + .WithMany() + .HasForeignKey("ClosedByUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "OpenedBy") + .WithMany() + .HasForeignKey("OpenedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("ClosedBy"); + + b.Navigation("OpenedBy"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Ingredient", "Ingredient") + .WithMany("Movements") + .HasForeignKey("IngredientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Ingredient"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("SubscriptionPayments") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Tables") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Tables") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.TableSection", "Section") + .WithMany("Tables") + .HasForeignKey("SectionId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Section"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany() + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Customer"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Sections") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Taxes") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.WebhookLog", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Navigation("Orders"); + + b.Navigation("Sections"); + + b.Navigation("Staff"); + + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Navigation("Branches"); + + b.Navigation("Coupons"); + + b.Navigation("Customers"); + + b.Navigation("Employees"); + + b.Navigation("Ingredients"); + + b.Navigation("MenuCategories"); + + b.Navigation("MenuItems"); + + b.Navigation("Orders"); + + b.Navigation("Reviews"); + + b.Navigation("SubscriptionPayments"); + + b.Navigation("Tables"); + + b.Navigation("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Navigation("Attendances"); + + b.Navigation("LeaveRequests"); + + b.Navigation("Orders"); + + b.Navigation("Salaries"); + + b.Navigation("Schedules"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Navigation("Movements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Navigation("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Navigation("BranchOverrides"); + + b.Navigation("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Navigation("Items"); + + b.Navigation("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Navigation("MenuCategories"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260522115531_DeliveryPlatformIntegration.cs b/src/Meezi.Infrastructure/Data/Migrations/20260522115531_DeliveryPlatformIntegration.cs new file mode 100644 index 0000000..a77689b --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260522115531_DeliveryPlatformIntegration.cs @@ -0,0 +1,200 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + /// + public partial class DeliveryPlatformIntegration : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_Orders_CafeId", + table: "Orders"); + + migrationBuilder.AddColumn( + name: "DeliveryMetaJson", + table: "Orders", + type: "character varying(4000)", + maxLength: 4000, + nullable: true); + + migrationBuilder.AddColumn( + name: "DeliveryPlatform", + table: "Orders", + type: "integer", + nullable: true); + + migrationBuilder.AddColumn( + name: "ExternalOrderId", + table: "Orders", + type: "character varying(120)", + maxLength: 120, + nullable: true); + + migrationBuilder.AddColumn( + name: "PlatformCommission", + table: "Orders", + type: "numeric(18,2)", + precision: 18, + scale: 2, + nullable: false, + defaultValue: 0m); + + migrationBuilder.AlterColumn( + name: "DefaultTaxRate", + table: "Cafes", + type: "numeric(5,2)", + precision: 5, + scale: 2, + nullable: false, + oldClrType: typeof(decimal), + oldType: "numeric"); + + migrationBuilder.AddColumn( + name: "DigikalaVendorId", + table: "Cafes", + type: "character varying(100)", + maxLength: 100, + nullable: true); + + migrationBuilder.AddColumn( + name: "Tap30VendorId", + table: "Cafes", + type: "character varying(100)", + maxLength: 100, + nullable: true); + + migrationBuilder.CreateTable( + name: "DeliveryCommissionRates", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + Platform = table.Column(type: "integer", nullable: false), + RatePercent = table.Column(type: "numeric(5,2)", precision: 5, scale: 2, nullable: false), + IsActive = table.Column(type: "boolean", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CafeId = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_DeliveryCommissionRates", x => x.Id); + table.ForeignKey( + name: "FK_DeliveryCommissionRates_Cafes_CafeId", + column: x => x.CafeId, + principalTable: "Cafes", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "WebhookLogs", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + CafeId = table.Column(type: "text", nullable: true), + Platform = table.Column(type: "integer", nullable: false), + RawBody = table.Column(type: "text", nullable: false), + SignatureHeader = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + SignatureValid = table.Column(type: "boolean", nullable: false), + Processed = table.Column(type: "boolean", nullable: false), + Success = table.Column(type: "boolean", nullable: false), + ErrorMessage = table.Column(type: "character varying(2000)", maxLength: 2000, nullable: true), + AttemptCount = table.Column(type: "integer", nullable: false), + ExternalOrderId = table.Column(type: "character varying(120)", maxLength: 120, nullable: true), + MeeziOrderId = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + ProcessedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_WebhookLogs", x => x.Id); + table.ForeignKey( + name: "FK_WebhookLogs_Cafes_CafeId", + column: x => x.CafeId, + principalTable: "Cafes", + principalColumn: "Id"); + }); + + migrationBuilder.CreateIndex( + name: "IX_Orders_CafeId_DeliveryPlatform_ExternalOrderId", + table: "Orders", + columns: new[] { "CafeId", "DeliveryPlatform", "ExternalOrderId" }); + + migrationBuilder.CreateIndex( + name: "IX_DeliveryCommissionRates_CafeId_Platform", + table: "DeliveryCommissionRates", + columns: new[] { "CafeId", "Platform" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_WebhookLogs_CafeId", + table: "WebhookLogs", + column: "CafeId"); + + migrationBuilder.CreateIndex( + name: "IX_WebhookLogs_Platform_CreatedAt", + table: "WebhookLogs", + columns: new[] { "Platform", "CreatedAt" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "DeliveryCommissionRates"); + + migrationBuilder.DropTable( + name: "WebhookLogs"); + + migrationBuilder.DropIndex( + name: "IX_Orders_CafeId_DeliveryPlatform_ExternalOrderId", + table: "Orders"); + + migrationBuilder.DropColumn( + name: "DeliveryMetaJson", + table: "Orders"); + + migrationBuilder.DropColumn( + name: "DeliveryPlatform", + table: "Orders"); + + migrationBuilder.DropColumn( + name: "ExternalOrderId", + table: "Orders"); + + migrationBuilder.DropColumn( + name: "PlatformCommission", + table: "Orders"); + + migrationBuilder.DropColumn( + name: "DigikalaVendorId", + table: "Cafes"); + + migrationBuilder.DropColumn( + name: "Tap30VendorId", + table: "Cafes"); + + migrationBuilder.AlterColumn( + name: "DefaultTaxRate", + table: "Cafes", + type: "numeric", + nullable: false, + oldClrType: typeof(decimal), + oldType: "numeric(5,2)", + oldPrecision: 5, + oldScale: 2); + + migrationBuilder.CreateIndex( + name: "IX_Orders_CafeId", + table: "Orders", + column: "CafeId"); + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260522160252_SystemAdminPlatform.Designer.cs b/src/Meezi.Infrastructure/Data/Migrations/20260522160252_SystemAdminPlatform.Designer.cs new file mode 100644 index 0000000..b0d39ca --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260522160252_SystemAdminPlatform.Designer.cs @@ -0,0 +1,2540 @@ +// +using System; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260522160252_SystemAdminPlatform")] + partial class SystemAdminPlatform + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ClockIn") + .HasColumnType("timestamp with time zone"); + + b.Property("ClockOut") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "Date") + .IsUnique(); + + b.ToTable("Attendances"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccentColor") + .HasColumnType("text"); + + b.Property("Address") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("AutoCutEnabled") + .HasColumnType("boolean"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("City") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("KitchenPrinterIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("KitchenPrinterPort") + .HasColumnType("integer"); + + b.Property("LogoUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("PaperWidthMm") + .HasColumnType("integer"); + + b.Property("Phone") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("PosDeviceIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("PosDevicePort") + .HasColumnType("integer"); + + b.Property("ReceiptFooter") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptHeader") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptPrinterIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("ReceiptPrinterPort") + .HasColumnType("integer"); + + b.Property("TaxRate") + .HasColumnType("numeric"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WelcomeText") + .HasColumnType("text"); + + b.Property("WifiPassword") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "IsActive"); + + b.ToTable("Branches"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.BranchMenuItemOverride", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("PriceOverride") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("SortOrderOverride") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedByUserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("BranchId", "MenuItemId") + .IsUnique(); + + b.ToTable("BranchMenuItemOverrides"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("AllowBranchTaxOverride") + .HasColumnType("boolean"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("CoverImageUrl") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DefaultTaxRate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DigikalaVendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("IsSuspended") + .HasColumnType("boolean"); + + b.Property("IsVerified") + .HasColumnType("boolean"); + + b.Property("LogoUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("PlanExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("PreferredLanguage") + .IsRequired() + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SnappfoodVendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Tap30VendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ThemeJson") + .HasMaxLength(8000) + .HasColumnType("character varying(8000)"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Cafes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeFeatureOverride", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FeatureKey") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "FeatureKey") + .IsUnique(); + + b.ToTable("CafeFeatureOverrides"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AuthorName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("AuthorPhone") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Comment") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerRepliedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerReply") + .HasColumnType("text"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "CreatedAt"); + + b.ToTable("CafeReviews"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CashTransaction", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("ReferenceId") + .HasColumnType("text"); + + b.Property("ShiftId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("ShiftId"); + + b.HasIndex("CafeId", "BranchId"); + + b.ToTable("CashTransactions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MaxDiscount") + .HasColumnType("numeric"); + + b.Property("MinOrderAmount") + .HasColumnType("numeric"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TargetGroup") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UsageLimit") + .HasColumnType("integer"); + + b.Property("UsedCount") + .HasColumnType("integer"); + + b.Property("Value") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Code") + .IsUnique(); + + b.ToTable("Coupons"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BirthDateJalali") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Group") + .HasColumnType("integer"); + + b.Property("LoyaltyPoints") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReferredBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DailyReport", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AvgOrderValue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CardRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CashRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreditRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GeneratedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("NetIncome") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TopProducts") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("TotalExpenses") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TotalOrders") + .HasColumnType("integer"); + + b.Property("TotalRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TotalVoids") + .HasColumnType("integer"); + + b.Property("VoidAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "BranchId", "Date") + .IsUnique(); + + b.ToTable("DailyReports"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DeliveryCommissionRate", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Platform") + .HasColumnType("integer"); + + b.Property("RatePercent") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Platform") + .IsUnique(); + + b.ToTable("DeliveryCommissionRates"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("PinCode") + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Employees"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Deductions") + .HasColumnType("numeric"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsPaid") + .HasColumnType("boolean"); + + b.Property("MonthYear") + .IsRequired() + .HasColumnType("text"); + + b.Property("NetSalary") + .HasColumnType("numeric"); + + b.Property("OvertimePay") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "MonthYear") + .IsUnique(); + + b.ToTable("EmployeeSalaries"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSchedule", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DayOfWeek") + .HasColumnType("integer"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShiftType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "DayOfWeek") + .IsUnique(); + + b.ToTable("EmployeeSchedules", (string)null); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Expense", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Category") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptImageUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ShiftId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("ShiftId"); + + b.HasIndex("CafeId", "BranchId", "CreatedAt"); + + b.ToTable("Expenses"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("QuantityOnHand") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("ReorderLevel") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("Unit") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Ingredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndDate") + .HasColumnType("date"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("ReviewedBy") + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId"); + + b.ToTable("LeaveRequests"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("Icon") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("IconPresetId") + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("IconStyle") + .HasMaxLength(16) + .HasColumnType("character varying(16)"); + + b.Property("ImageUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("TaxId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("TaxId"); + + b.ToTable("MenuCategories"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CategoryId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("CategoryId"); + + b.ToTable("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CouponId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeliveryMetaJson") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("DeliveryPlatform") + .HasColumnType("integer"); + + b.Property("DiscountAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("EmployeeId") + .HasColumnType("text"); + + b.Property("ExternalOrderId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("GuestName") + .HasColumnType("text"); + + b.Property("GuestPhone") + .HasColumnType("text"); + + b.Property("OrderType") + .HasColumnType("integer"); + + b.Property("PlatformCommission") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ReservationId") + .HasColumnType("text"); + + b.Property("SnappfoodOrderId") + .HasColumnType("text"); + + b.Property("Source") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subtotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("TaxTotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Total") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CouponId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("ReservationId"); + + b.HasIndex("TableId"); + + b.HasIndex("CafeId", "DeliveryPlatform", "ExternalOrderId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsVoided") + .HasColumnType("boolean"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UnitPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VoidedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VoidedByUserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Reference") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.PlatformFeature", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DisplayNameEn") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DisplayNameFa") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("IsEnabledGlobally") + .HasColumnType("boolean"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("ModuleGroup") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("PlatformFeatures"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.PlatformPlanDefinition", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DisplayNameEn") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DisplayNameFa") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("FeaturesJson") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsBillableOnline") + .HasColumnType("boolean"); + + b.Property("LimitsJson") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("MonthlyPriceToman") + .HasPrecision(18) + .HasColumnType("numeric(18,0)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("Tier") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Tier") + .IsUnique(); + + b.ToTable("PlatformPlanDefinitions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.PlatformSetting", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DescriptionFa") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(8000) + .HasColumnType("character varying(8000)"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("PlatformSettings"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.QueueTicket", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerLabel") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedByUserId") + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("integer"); + + b.Property("OrderId") + .HasColumnType("text"); + + b.Property("ServiceDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("OrderId"); + + b.HasIndex("CafeId", "BranchId", "ServiceDate", "Number") + .IsUnique(); + + b.ToTable("QueueTickets"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClosedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ClosedByUserId") + .HasColumnType("text"); + + b.Property("ClosingCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Discrepancy") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ExpectedCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("OpenedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OpenedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("OpeningCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("ClosedByUserId"); + + b.HasIndex("OpenedByUserId"); + + b.HasIndex("BranchId", "Status"); + + b.ToTable("RegisterShifts", (string)null); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Delta") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("IngredientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Note") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("IngredientId"); + + b.ToTable("StockMovements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AmountRials") + .HasColumnType("bigint"); + + b.Property("AmountToman") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Authority") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Months") + .HasColumnType("integer"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("RefId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Authority"); + + b.HasIndex("CafeId"); + + b.ToTable("SubscriptionPayments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicket", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AssignedAdminId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClosedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByEmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Priority") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subject") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedByEmployeeId"); + + b.HasIndex("CafeId", "Status", "UpdatedAt"); + + b.ToTable("SupportTickets"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicketMessage", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Body") + .IsRequired() + .HasMaxLength(8000) + .HasColumnType("character varying(8000)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SenderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("SenderKind") + .HasColumnType("integer"); + + b.Property("TicketId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("TicketId", "CreatedAt"); + + b.ToTable("SupportTicketMessages"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SystemAdmin", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.HasKey("Id"); + + b.HasIndex("Phone") + .IsUnique(); + + b.ToTable("SystemAdmins"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Floor") + .HasColumnType("text"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsCleaning") + .HasColumnType("boolean"); + + b.Property("Number") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("QrCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("SectionId") + .HasColumnType("text"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("QrCode") + .IsUnique(); + + b.HasIndex("SectionId"); + + b.HasIndex("BranchId", "SectionId", "SortOrder"); + + b.ToTable("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GuestName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GuestPhone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("PartySize") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("time without time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("TableId"); + + b.HasIndex("CafeId", "Date", "Time"); + + b.ToTable("TableReservations"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("BranchId", "Name"); + + b.ToTable("TableSections"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsCompound") + .HasColumnType("boolean"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("IsRequired") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.WebhookLog", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AttemptCount") + .HasColumnType("integer"); + + b.Property("CafeId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("ExternalOrderId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("MeeziOrderId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Platform") + .HasColumnType("integer"); + + b.Property("Processed") + .HasColumnType("boolean"); + + b.Property("ProcessedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RawBody") + .IsRequired() + .HasColumnType("text"); + + b.Property("SignatureHeader") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("SignatureValid") + .HasColumnType("boolean"); + + b.Property("Success") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("Platform", "CreatedAt"); + + b.ToTable("WebhookLogs"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Attendances") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Branches") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.BranchMenuItemOverride", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("BranchOverrides") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("MenuItem"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeFeatureOverride", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", null) + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Reviews") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CashTransaction", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Shift", "Shift") + .WithMany("Transactions") + .HasForeignKey("ShiftId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Shift"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Coupons") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Customers") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DailyReport", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DeliveryCommissionRate", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Staff") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Employees") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Salaries") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSchedule", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Schedules") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Expense", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Shift", "Shift") + .WithMany() + .HasForeignKey("ShiftId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Shift"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Ingredients") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("LeaveRequests") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuCategories") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Tax", "Tax") + .WithMany("MenuCategories") + .HasForeignKey("TaxId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Tax"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuItems") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuCategory", "Category") + .WithMany("MenuItems") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Orders") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Orders") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Coupon", "Coupon") + .WithMany("Orders") + .HasForeignKey("CouponId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Orders") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.TableReservation", "Reservation") + .WithMany() + .HasForeignKey("ReservationId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany("Orders") + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Coupon"); + + b.Navigation("Customer"); + + b.Navigation("Employee"); + + b.Navigation("Reservation"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("OrderItems") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Items") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MenuItem"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Payments") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.QueueTicket", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Employee", "ClosedBy") + .WithMany() + .HasForeignKey("ClosedByUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "OpenedBy") + .WithMany() + .HasForeignKey("OpenedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("ClosedBy"); + + b.Navigation("OpenedBy"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Ingredient", "Ingredient") + .WithMany("Movements") + .HasForeignKey("IngredientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Ingredient"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("SubscriptionPayments") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicket", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Employee", "CreatedByEmployee") + .WithMany() + .HasForeignKey("CreatedByEmployeeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("CreatedByEmployee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicketMessage", b => + { + b.HasOne("Meezi.Core.Entities.SupportTicket", "Ticket") + .WithMany("Messages") + .HasForeignKey("TicketId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ticket"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Tables") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Tables") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.TableSection", "Section") + .WithMany("Tables") + .HasForeignKey("SectionId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Section"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany() + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Customer"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Sections") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Taxes") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.WebhookLog", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Navigation("Orders"); + + b.Navigation("Sections"); + + b.Navigation("Staff"); + + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Navigation("Branches"); + + b.Navigation("Coupons"); + + b.Navigation("Customers"); + + b.Navigation("Employees"); + + b.Navigation("Ingredients"); + + b.Navigation("MenuCategories"); + + b.Navigation("MenuItems"); + + b.Navigation("Orders"); + + b.Navigation("Reviews"); + + b.Navigation("SubscriptionPayments"); + + b.Navigation("Tables"); + + b.Navigation("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Navigation("Attendances"); + + b.Navigation("LeaveRequests"); + + b.Navigation("Orders"); + + b.Navigation("Salaries"); + + b.Navigation("Schedules"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Navigation("Movements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Navigation("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Navigation("BranchOverrides"); + + b.Navigation("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Navigation("Items"); + + b.Navigation("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicket", b => + { + b.Navigation("Messages"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Navigation("MenuCategories"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260522160252_SystemAdminPlatform.cs b/src/Meezi.Infrastructure/Data/Migrations/20260522160252_SystemAdminPlatform.cs new file mode 100644 index 0000000..af55357 --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260522160252_SystemAdminPlatform.cs @@ -0,0 +1,249 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + /// + public partial class SystemAdminPlatform : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "IsSuspended", + table: "Cafes", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.CreateTable( + name: "CafeFeatureOverrides", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + FeatureKey = table.Column(type: "character varying(80)", maxLength: 80, nullable: false), + IsEnabled = table.Column(type: "boolean", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CafeId = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_CafeFeatureOverrides", x => x.Id); + table.ForeignKey( + name: "FK_CafeFeatureOverrides_Cafes_CafeId", + column: x => x.CafeId, + principalTable: "Cafes", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "PlatformFeatures", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + Key = table.Column(type: "character varying(80)", maxLength: 80, nullable: false), + DisplayNameFa = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + DisplayNameEn = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + ModuleGroup = table.Column(type: "character varying(60)", maxLength: 60, nullable: false), + IsEnabledGlobally = table.Column(type: "boolean", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_PlatformFeatures", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "PlatformPlanDefinitions", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + Tier = table.Column(type: "integer", nullable: false), + DisplayNameFa = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + DisplayNameEn = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + MonthlyPriceToman = table.Column(type: "numeric(18,0)", precision: 18, scale: 0, nullable: false), + IsBillableOnline = table.Column(type: "boolean", nullable: false), + IsActive = table.Column(type: "boolean", nullable: false), + SortOrder = table.Column(type: "integer", nullable: false), + LimitsJson = table.Column(type: "character varying(4000)", maxLength: 4000, nullable: false), + FeaturesJson = table.Column(type: "character varying(4000)", maxLength: 4000, nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_PlatformPlanDefinitions", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "PlatformSettings", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + Key = table.Column(type: "character varying(120)", maxLength: 120, nullable: false), + Value = table.Column(type: "character varying(8000)", maxLength: 8000, nullable: false), + Category = table.Column(type: "character varying(60)", maxLength: 60, nullable: false), + DescriptionFa = table.Column(type: "character varying(500)", maxLength: 500, nullable: true), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_PlatformSettings", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "SupportTickets", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + Subject = table.Column(type: "character varying(300)", maxLength: 300, nullable: false), + Status = table.Column(type: "integer", nullable: false), + Priority = table.Column(type: "integer", nullable: false), + CreatedByEmployeeId = table.Column(type: "text", nullable: false), + AssignedAdminId = table.Column(type: "text", nullable: true), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false), + ClosedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CafeId = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_SupportTickets", x => x.Id); + table.ForeignKey( + name: "FK_SupportTickets_Cafes_CafeId", + column: x => x.CafeId, + principalTable: "Cafes", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_SupportTickets_Employees_CreatedByEmployeeId", + column: x => x.CreatedByEmployeeId, + principalTable: "Employees", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "SystemAdmins", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + Name = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + Phone = table.Column(type: "character varying(20)", maxLength: 20, nullable: false), + IsActive = table.Column(type: "boolean", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_SystemAdmins", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "SupportTicketMessages", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + TicketId = table.Column(type: "text", nullable: false), + SenderKind = table.Column(type: "integer", nullable: false), + SenderId = table.Column(type: "text", nullable: false), + Body = table.Column(type: "character varying(8000)", maxLength: 8000, nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_SupportTicketMessages", x => x.Id); + table.ForeignKey( + name: "FK_SupportTicketMessages_SupportTickets_TicketId", + column: x => x.TicketId, + principalTable: "SupportTickets", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_CafeFeatureOverrides_CafeId_FeatureKey", + table: "CafeFeatureOverrides", + columns: new[] { "CafeId", "FeatureKey" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_PlatformFeatures_Key", + table: "PlatformFeatures", + column: "Key", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_PlatformPlanDefinitions_Tier", + table: "PlatformPlanDefinitions", + column: "Tier", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_PlatformSettings_Key", + table: "PlatformSettings", + column: "Key", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_SupportTicketMessages_TicketId_CreatedAt", + table: "SupportTicketMessages", + columns: new[] { "TicketId", "CreatedAt" }); + + migrationBuilder.CreateIndex( + name: "IX_SupportTickets_CafeId_Status_UpdatedAt", + table: "SupportTickets", + columns: new[] { "CafeId", "Status", "UpdatedAt" }); + + migrationBuilder.CreateIndex( + name: "IX_SupportTickets_CreatedByEmployeeId", + table: "SupportTickets", + column: "CreatedByEmployeeId"); + + migrationBuilder.CreateIndex( + name: "IX_SystemAdmins_Phone", + table: "SystemAdmins", + column: "Phone", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "CafeFeatureOverrides"); + + migrationBuilder.DropTable( + name: "PlatformFeatures"); + + migrationBuilder.DropTable( + name: "PlatformPlanDefinitions"); + + migrationBuilder.DropTable( + name: "PlatformSettings"); + + migrationBuilder.DropTable( + name: "SupportTicketMessages"); + + migrationBuilder.DropTable( + name: "SystemAdmins"); + + migrationBuilder.DropTable( + name: "SupportTickets"); + + migrationBuilder.DropColumn( + name: "IsSuspended", + table: "Cafes"); + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260522161356_OrderNotificationsAndGuestTracking.Designer.cs b/src/Meezi.Infrastructure/Data/Migrations/20260522161356_OrderNotificationsAndGuestTracking.Designer.cs new file mode 100644 index 0000000..6fbc319 --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260522161356_OrderNotificationsAndGuestTracking.Designer.cs @@ -0,0 +1,2599 @@ +// +using System; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260522161356_OrderNotificationsAndGuestTracking")] + partial class OrderNotificationsAndGuestTracking + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ClockIn") + .HasColumnType("timestamp with time zone"); + + b.Property("ClockOut") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "Date") + .IsUnique(); + + b.ToTable("Attendances"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccentColor") + .HasColumnType("text"); + + b.Property("Address") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("AutoCutEnabled") + .HasColumnType("boolean"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("City") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("KitchenPrinterIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("KitchenPrinterPort") + .HasColumnType("integer"); + + b.Property("LogoUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("PaperWidthMm") + .HasColumnType("integer"); + + b.Property("Phone") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("PosDeviceIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("PosDevicePort") + .HasColumnType("integer"); + + b.Property("ReceiptFooter") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptHeader") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptPrinterIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("ReceiptPrinterPort") + .HasColumnType("integer"); + + b.Property("TaxRate") + .HasColumnType("numeric"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WelcomeText") + .HasColumnType("text"); + + b.Property("WifiPassword") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "IsActive"); + + b.ToTable("Branches"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.BranchMenuItemOverride", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("PriceOverride") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("SortOrderOverride") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedByUserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("BranchId", "MenuItemId") + .IsUnique(); + + b.ToTable("BranchMenuItemOverrides"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("AllowBranchTaxOverride") + .HasColumnType("boolean"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("CoverImageUrl") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DefaultTaxRate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DigikalaVendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("IsSuspended") + .HasColumnType("boolean"); + + b.Property("IsVerified") + .HasColumnType("boolean"); + + b.Property("LogoUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("PlanExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("PreferredLanguage") + .IsRequired() + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SnappfoodVendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Tap30VendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ThemeJson") + .HasMaxLength(8000) + .HasColumnType("character varying(8000)"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Cafes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeFeatureOverride", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FeatureKey") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "FeatureKey") + .IsUnique(); + + b.ToTable("CafeFeatureOverrides"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeNotification", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Body") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsRead") + .HasColumnType("boolean"); + + b.Property("ReadAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReferenceId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("TableNumber") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "IsRead", "CreatedAt"); + + b.ToTable("CafeNotifications"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AuthorName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("AuthorPhone") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Comment") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerRepliedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerReply") + .HasColumnType("text"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "CreatedAt"); + + b.ToTable("CafeReviews"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CashTransaction", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("ReferenceId") + .HasColumnType("text"); + + b.Property("ShiftId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("ShiftId"); + + b.HasIndex("CafeId", "BranchId"); + + b.ToTable("CashTransactions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MaxDiscount") + .HasColumnType("numeric"); + + b.Property("MinOrderAmount") + .HasColumnType("numeric"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TargetGroup") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UsageLimit") + .HasColumnType("integer"); + + b.Property("UsedCount") + .HasColumnType("integer"); + + b.Property("Value") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Code") + .IsUnique(); + + b.ToTable("Coupons"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BirthDateJalali") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Group") + .HasColumnType("integer"); + + b.Property("LoyaltyPoints") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReferredBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DailyReport", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AvgOrderValue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CardRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CashRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreditRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GeneratedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("NetIncome") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TopProducts") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("TotalExpenses") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TotalOrders") + .HasColumnType("integer"); + + b.Property("TotalRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TotalVoids") + .HasColumnType("integer"); + + b.Property("VoidAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "BranchId", "Date") + .IsUnique(); + + b.ToTable("DailyReports"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DeliveryCommissionRate", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Platform") + .HasColumnType("integer"); + + b.Property("RatePercent") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Platform") + .IsUnique(); + + b.ToTable("DeliveryCommissionRates"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("PinCode") + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Employees"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Deductions") + .HasColumnType("numeric"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsPaid") + .HasColumnType("boolean"); + + b.Property("MonthYear") + .IsRequired() + .HasColumnType("text"); + + b.Property("NetSalary") + .HasColumnType("numeric"); + + b.Property("OvertimePay") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "MonthYear") + .IsUnique(); + + b.ToTable("EmployeeSalaries"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSchedule", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DayOfWeek") + .HasColumnType("integer"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShiftType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "DayOfWeek") + .IsUnique(); + + b.ToTable("EmployeeSchedules", (string)null); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Expense", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Category") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptImageUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ShiftId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("ShiftId"); + + b.HasIndex("CafeId", "BranchId", "CreatedAt"); + + b.ToTable("Expenses"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("QuantityOnHand") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("ReorderLevel") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("Unit") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Ingredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndDate") + .HasColumnType("date"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("ReviewedBy") + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId"); + + b.ToTable("LeaveRequests"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("Icon") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("IconPresetId") + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("IconStyle") + .HasMaxLength(16) + .HasColumnType("character varying(16)"); + + b.Property("ImageUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("TaxId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("TaxId"); + + b.ToTable("MenuCategories"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CategoryId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("CategoryId"); + + b.ToTable("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CouponId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeliveryMetaJson") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("DeliveryPlatform") + .HasColumnType("integer"); + + b.Property("DiscountAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("EmployeeId") + .HasColumnType("text"); + + b.Property("ExternalOrderId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("GuestName") + .HasColumnType("text"); + + b.Property("GuestPhone") + .HasColumnType("text"); + + b.Property("GuestTrackingToken") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("OrderType") + .HasColumnType("integer"); + + b.Property("PlatformCommission") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ReservationId") + .HasColumnType("text"); + + b.Property("SnappfoodOrderId") + .HasColumnType("text"); + + b.Property("Source") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("StatusUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Subtotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("TaxTotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Total") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CouponId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("GuestTrackingToken"); + + b.HasIndex("ReservationId"); + + b.HasIndex("TableId"); + + b.HasIndex("CafeId", "DeliveryPlatform", "ExternalOrderId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsVoided") + .HasColumnType("boolean"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UnitPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VoidedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VoidedByUserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Reference") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.PlatformFeature", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DisplayNameEn") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DisplayNameFa") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("IsEnabledGlobally") + .HasColumnType("boolean"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("ModuleGroup") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("PlatformFeatures"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.PlatformPlanDefinition", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DisplayNameEn") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DisplayNameFa") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("FeaturesJson") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsBillableOnline") + .HasColumnType("boolean"); + + b.Property("LimitsJson") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("MonthlyPriceToman") + .HasPrecision(18) + .HasColumnType("numeric(18,0)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("Tier") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Tier") + .IsUnique(); + + b.ToTable("PlatformPlanDefinitions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.PlatformSetting", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DescriptionFa") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(8000) + .HasColumnType("character varying(8000)"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("PlatformSettings"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.QueueTicket", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerLabel") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedByUserId") + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("integer"); + + b.Property("OrderId") + .HasColumnType("text"); + + b.Property("ServiceDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("OrderId"); + + b.HasIndex("CafeId", "BranchId", "ServiceDate", "Number") + .IsUnique(); + + b.ToTable("QueueTickets"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClosedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ClosedByUserId") + .HasColumnType("text"); + + b.Property("ClosingCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Discrepancy") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ExpectedCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("OpenedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OpenedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("OpeningCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("ClosedByUserId"); + + b.HasIndex("OpenedByUserId"); + + b.HasIndex("BranchId", "Status"); + + b.ToTable("RegisterShifts", (string)null); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Delta") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("IngredientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Note") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("IngredientId"); + + b.ToTable("StockMovements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AmountRials") + .HasColumnType("bigint"); + + b.Property("AmountToman") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Authority") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Months") + .HasColumnType("integer"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("RefId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Authority"); + + b.HasIndex("CafeId"); + + b.ToTable("SubscriptionPayments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicket", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AssignedAdminId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClosedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByEmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Priority") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subject") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedByEmployeeId"); + + b.HasIndex("CafeId", "Status", "UpdatedAt"); + + b.ToTable("SupportTickets"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicketMessage", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Body") + .IsRequired() + .HasMaxLength(8000) + .HasColumnType("character varying(8000)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SenderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("SenderKind") + .HasColumnType("integer"); + + b.Property("TicketId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("TicketId", "CreatedAt"); + + b.ToTable("SupportTicketMessages"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SystemAdmin", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.HasKey("Id"); + + b.HasIndex("Phone") + .IsUnique(); + + b.ToTable("SystemAdmins"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Floor") + .HasColumnType("text"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsCleaning") + .HasColumnType("boolean"); + + b.Property("Number") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("QrCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("SectionId") + .HasColumnType("text"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("QrCode") + .IsUnique(); + + b.HasIndex("SectionId"); + + b.HasIndex("BranchId", "SectionId", "SortOrder"); + + b.ToTable("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GuestName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GuestPhone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("PartySize") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("time without time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("TableId"); + + b.HasIndex("CafeId", "Date", "Time"); + + b.ToTable("TableReservations"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("BranchId", "Name"); + + b.ToTable("TableSections"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsCompound") + .HasColumnType("boolean"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("IsRequired") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.WebhookLog", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AttemptCount") + .HasColumnType("integer"); + + b.Property("CafeId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("ExternalOrderId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("MeeziOrderId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Platform") + .HasColumnType("integer"); + + b.Property("Processed") + .HasColumnType("boolean"); + + b.Property("ProcessedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RawBody") + .IsRequired() + .HasColumnType("text"); + + b.Property("SignatureHeader") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("SignatureValid") + .HasColumnType("boolean"); + + b.Property("Success") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("Platform", "CreatedAt"); + + b.ToTable("WebhookLogs"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Attendances") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Branches") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.BranchMenuItemOverride", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("BranchOverrides") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("MenuItem"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeFeatureOverride", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", null) + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Reviews") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CashTransaction", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Shift", "Shift") + .WithMany("Transactions") + .HasForeignKey("ShiftId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Shift"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Coupons") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Customers") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DailyReport", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DeliveryCommissionRate", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Staff") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Employees") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Salaries") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSchedule", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Schedules") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Expense", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Shift", "Shift") + .WithMany() + .HasForeignKey("ShiftId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Shift"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Ingredients") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("LeaveRequests") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuCategories") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Tax", "Tax") + .WithMany("MenuCategories") + .HasForeignKey("TaxId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Tax"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuItems") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuCategory", "Category") + .WithMany("MenuItems") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Orders") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Orders") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Coupon", "Coupon") + .WithMany("Orders") + .HasForeignKey("CouponId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Orders") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.TableReservation", "Reservation") + .WithMany() + .HasForeignKey("ReservationId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany("Orders") + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Coupon"); + + b.Navigation("Customer"); + + b.Navigation("Employee"); + + b.Navigation("Reservation"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("OrderItems") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Items") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MenuItem"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Payments") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.QueueTicket", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Employee", "ClosedBy") + .WithMany() + .HasForeignKey("ClosedByUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "OpenedBy") + .WithMany() + .HasForeignKey("OpenedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("ClosedBy"); + + b.Navigation("OpenedBy"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Ingredient", "Ingredient") + .WithMany("Movements") + .HasForeignKey("IngredientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Ingredient"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("SubscriptionPayments") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicket", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Employee", "CreatedByEmployee") + .WithMany() + .HasForeignKey("CreatedByEmployeeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("CreatedByEmployee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicketMessage", b => + { + b.HasOne("Meezi.Core.Entities.SupportTicket", "Ticket") + .WithMany("Messages") + .HasForeignKey("TicketId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ticket"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Tables") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Tables") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.TableSection", "Section") + .WithMany("Tables") + .HasForeignKey("SectionId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Section"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany() + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Customer"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Sections") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Taxes") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.WebhookLog", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Navigation("Orders"); + + b.Navigation("Sections"); + + b.Navigation("Staff"); + + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Navigation("Branches"); + + b.Navigation("Coupons"); + + b.Navigation("Customers"); + + b.Navigation("Employees"); + + b.Navigation("Ingredients"); + + b.Navigation("MenuCategories"); + + b.Navigation("MenuItems"); + + b.Navigation("Orders"); + + b.Navigation("Reviews"); + + b.Navigation("SubscriptionPayments"); + + b.Navigation("Tables"); + + b.Navigation("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Navigation("Attendances"); + + b.Navigation("LeaveRequests"); + + b.Navigation("Orders"); + + b.Navigation("Salaries"); + + b.Navigation("Schedules"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Navigation("Movements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Navigation("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Navigation("BranchOverrides"); + + b.Navigation("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Navigation("Items"); + + b.Navigation("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicket", b => + { + b.Navigation("Messages"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Navigation("MenuCategories"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260522161356_OrderNotificationsAndGuestTracking.cs b/src/Meezi.Infrastructure/Data/Migrations/20260522161356_OrderNotificationsAndGuestTracking.cs new file mode 100644 index 0000000..7b7c2f8 --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260522161356_OrderNotificationsAndGuestTracking.cs @@ -0,0 +1,79 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + /// + public partial class OrderNotificationsAndGuestTracking : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "GuestTrackingToken", + table: "Orders", + type: "character varying(64)", + maxLength: 64, + nullable: true); + + migrationBuilder.AddColumn( + name: "StatusUpdatedAt", + table: "Orders", + type: "timestamp with time zone", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.CreateTable( + name: "CafeNotifications", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + Type = table.Column(type: "character varying(60)", maxLength: 60, nullable: false), + Title = table.Column(type: "character varying(300)", maxLength: 300, nullable: false), + Body = table.Column(type: "character varying(1000)", maxLength: 1000, nullable: true), + ReferenceId = table.Column(type: "character varying(64)", maxLength: 64, nullable: true), + TableNumber = table.Column(type: "character varying(40)", maxLength: 40, nullable: true), + IsRead = table.Column(type: "boolean", nullable: false), + ReadAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CafeId = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_CafeNotifications", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_Orders_GuestTrackingToken", + table: "Orders", + column: "GuestTrackingToken"); + + migrationBuilder.CreateIndex( + name: "IX_CafeNotifications_CafeId_IsRead_CreatedAt", + table: "CafeNotifications", + columns: new[] { "CafeId", "IsRead", "CreatedAt" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "CafeNotifications"); + + migrationBuilder.DropIndex( + name: "IX_Orders_GuestTrackingToken", + table: "Orders"); + + migrationBuilder.DropColumn( + name: "GuestTrackingToken", + table: "Orders"); + + migrationBuilder.DropColumn( + name: "StatusUpdatedAt", + table: "Orders"); + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260522174742_MenuItemRecipesAndIngredientCost.Designer.cs b/src/Meezi.Infrastructure/Data/Migrations/20260522174742_MenuItemRecipesAndIngredientCost.Designer.cs new file mode 100644 index 0000000..474821f --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260522174742_MenuItemRecipesAndIngredientCost.Designer.cs @@ -0,0 +1,2690 @@ +// +using System; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260522174742_MenuItemRecipesAndIngredientCost")] + partial class MenuItemRecipesAndIngredientCost + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ClockIn") + .HasColumnType("timestamp with time zone"); + + b.Property("ClockOut") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "Date") + .IsUnique(); + + b.ToTable("Attendances"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccentColor") + .HasColumnType("text"); + + b.Property("Address") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("AutoCutEnabled") + .HasColumnType("boolean"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("City") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("KitchenPrinterIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("KitchenPrinterPort") + .HasColumnType("integer"); + + b.Property("LogoUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("PaperWidthMm") + .HasColumnType("integer"); + + b.Property("Phone") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("PosDeviceIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("PosDevicePort") + .HasColumnType("integer"); + + b.Property("ReceiptFooter") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptHeader") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptPrinterIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("ReceiptPrinterPort") + .HasColumnType("integer"); + + b.Property("TaxRate") + .HasColumnType("numeric"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WelcomeText") + .HasColumnType("text"); + + b.Property("WifiPassword") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "IsActive"); + + b.ToTable("Branches"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.BranchMenuItemOverride", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("PriceOverride") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("SortOrderOverride") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedByUserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("BranchId", "MenuItemId") + .IsUnique(); + + b.ToTable("BranchMenuItemOverrides"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("AllowBranchTaxOverride") + .HasColumnType("boolean"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("CoverImageUrl") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DefaultTaxRate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DigikalaVendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("IsSuspended") + .HasColumnType("boolean"); + + b.Property("IsVerified") + .HasColumnType("boolean"); + + b.Property("LogoUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("PlanExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("PreferredLanguage") + .IsRequired() + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SnappfoodVendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Tap30VendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ThemeJson") + .HasMaxLength(8000) + .HasColumnType("character varying(8000)"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Cafes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeFeatureOverride", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FeatureKey") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "FeatureKey") + .IsUnique(); + + b.ToTable("CafeFeatureOverrides"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeNotification", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Body") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsRead") + .HasColumnType("boolean"); + + b.Property("ReadAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReferenceId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("TableNumber") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "IsRead", "CreatedAt"); + + b.ToTable("CafeNotifications"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AuthorName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("AuthorPhone") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Comment") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerRepliedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerReply") + .HasColumnType("text"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "CreatedAt"); + + b.ToTable("CafeReviews"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CashTransaction", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("ReferenceId") + .HasColumnType("text"); + + b.Property("ShiftId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("ShiftId"); + + b.HasIndex("CafeId", "BranchId"); + + b.ToTable("CashTransactions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MaxDiscount") + .HasColumnType("numeric"); + + b.Property("MinOrderAmount") + .HasColumnType("numeric"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TargetGroup") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UsageLimit") + .HasColumnType("integer"); + + b.Property("UsedCount") + .HasColumnType("integer"); + + b.Property("Value") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Code") + .IsUnique(); + + b.ToTable("Coupons"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BirthDateJalali") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Group") + .HasColumnType("integer"); + + b.Property("LoyaltyPoints") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReferredBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DailyReport", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AvgOrderValue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CardRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CashRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreditRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GeneratedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("NetIncome") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TopProducts") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("TotalExpenses") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TotalOrders") + .HasColumnType("integer"); + + b.Property("TotalRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TotalVoids") + .HasColumnType("integer"); + + b.Property("VoidAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "BranchId", "Date") + .IsUnique(); + + b.ToTable("DailyReports"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DeliveryCommissionRate", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Platform") + .HasColumnType("integer"); + + b.Property("RatePercent") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Platform") + .IsUnique(); + + b.ToTable("DeliveryCommissionRates"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("PinCode") + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Employees"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Deductions") + .HasColumnType("numeric"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsPaid") + .HasColumnType("boolean"); + + b.Property("MonthYear") + .IsRequired() + .HasColumnType("text"); + + b.Property("NetSalary") + .HasColumnType("numeric"); + + b.Property("OvertimePay") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "MonthYear") + .IsUnique(); + + b.ToTable("EmployeeSalaries"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSchedule", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DayOfWeek") + .HasColumnType("integer"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShiftType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "DayOfWeek") + .IsUnique(); + + b.ToTable("EmployeeSchedules", (string)null); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Expense", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Category") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptImageUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ShiftId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("ShiftId"); + + b.HasIndex("CafeId", "BranchId", "CreatedAt"); + + b.ToTable("Expenses"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LowStockWarningPercent") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ParLevel") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("QuantityOnHand") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("ReorderLevel") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("Unit") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("UnitCost") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Ingredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndDate") + .HasColumnType("date"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("ReviewedBy") + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId"); + + b.ToTable("LeaveRequests"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("Icon") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("IconPresetId") + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("IconStyle") + .HasMaxLength(16) + .HasColumnType("character varying(16)"); + + b.Property("ImageUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("TaxId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("TaxId"); + + b.ToTable("MenuCategories"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CategoryId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("CategoryId"); + + b.ToTable("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItemIngredient", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IngredientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("QuantityPerUnit") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.HasKey("Id"); + + b.HasIndex("IngredientId"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("CafeId", "MenuItemId", "IngredientId") + .IsUnique(); + + b.ToTable("MenuItemIngredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CouponId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeliveryMetaJson") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("DeliveryPlatform") + .HasColumnType("integer"); + + b.Property("DiscountAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("EmployeeId") + .HasColumnType("text"); + + b.Property("ExternalOrderId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("GuestName") + .HasColumnType("text"); + + b.Property("GuestPhone") + .HasColumnType("text"); + + b.Property("GuestTrackingToken") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("OrderType") + .HasColumnType("integer"); + + b.Property("PlatformCommission") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ReservationId") + .HasColumnType("text"); + + b.Property("SnappfoodOrderId") + .HasColumnType("text"); + + b.Property("Source") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("StatusUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Subtotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("TaxTotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Total") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CouponId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("GuestTrackingToken"); + + b.HasIndex("ReservationId"); + + b.HasIndex("TableId"); + + b.HasIndex("CafeId", "DeliveryPlatform", "ExternalOrderId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsVoided") + .HasColumnType("boolean"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UnitPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VoidedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VoidedByUserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Reference") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.PlatformFeature", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DisplayNameEn") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DisplayNameFa") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("IsEnabledGlobally") + .HasColumnType("boolean"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("ModuleGroup") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("PlatformFeatures"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.PlatformPlanDefinition", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DisplayNameEn") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DisplayNameFa") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("FeaturesJson") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsBillableOnline") + .HasColumnType("boolean"); + + b.Property("LimitsJson") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("MonthlyPriceToman") + .HasPrecision(18) + .HasColumnType("numeric(18,0)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("Tier") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Tier") + .IsUnique(); + + b.ToTable("PlatformPlanDefinitions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.PlatformSetting", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DescriptionFa") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(8000) + .HasColumnType("character varying(8000)"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("PlatformSettings"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.QueueTicket", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerLabel") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedByUserId") + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("integer"); + + b.Property("OrderId") + .HasColumnType("text"); + + b.Property("ServiceDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("OrderId"); + + b.HasIndex("CafeId", "BranchId", "ServiceDate", "Number") + .IsUnique(); + + b.ToTable("QueueTickets"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClosedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ClosedByUserId") + .HasColumnType("text"); + + b.Property("ClosingCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Discrepancy") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ExpectedCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("OpenedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OpenedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("OpeningCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("ClosedByUserId"); + + b.HasIndex("OpenedByUserId"); + + b.HasIndex("BranchId", "Status"); + + b.ToTable("RegisterShifts", (string)null); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Delta") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("IngredientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Kind") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("OrderId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("IngredientId"); + + b.HasIndex("CafeId", "OrderId"); + + b.ToTable("StockMovements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AmountRials") + .HasColumnType("bigint"); + + b.Property("AmountToman") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Authority") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Months") + .HasColumnType("integer"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("RefId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Authority"); + + b.HasIndex("CafeId"); + + b.ToTable("SubscriptionPayments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicket", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AssignedAdminId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClosedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByEmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Priority") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subject") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedByEmployeeId"); + + b.HasIndex("CafeId", "Status", "UpdatedAt"); + + b.ToTable("SupportTickets"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicketMessage", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Body") + .IsRequired() + .HasMaxLength(8000) + .HasColumnType("character varying(8000)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SenderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("SenderKind") + .HasColumnType("integer"); + + b.Property("TicketId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("TicketId", "CreatedAt"); + + b.ToTable("SupportTicketMessages"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SystemAdmin", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.HasKey("Id"); + + b.HasIndex("Phone") + .IsUnique(); + + b.ToTable("SystemAdmins"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Floor") + .HasColumnType("text"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsCleaning") + .HasColumnType("boolean"); + + b.Property("Number") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("QrCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("SectionId") + .HasColumnType("text"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("QrCode") + .IsUnique(); + + b.HasIndex("SectionId"); + + b.HasIndex("BranchId", "SectionId", "SortOrder"); + + b.ToTable("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GuestName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GuestPhone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("PartySize") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("time without time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("TableId"); + + b.HasIndex("CafeId", "Date", "Time"); + + b.ToTable("TableReservations"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("BranchId", "Name"); + + b.ToTable("TableSections"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsCompound") + .HasColumnType("boolean"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("IsRequired") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.WebhookLog", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AttemptCount") + .HasColumnType("integer"); + + b.Property("CafeId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("ExternalOrderId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("MeeziOrderId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Platform") + .HasColumnType("integer"); + + b.Property("Processed") + .HasColumnType("boolean"); + + b.Property("ProcessedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RawBody") + .IsRequired() + .HasColumnType("text"); + + b.Property("SignatureHeader") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("SignatureValid") + .HasColumnType("boolean"); + + b.Property("Success") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("Platform", "CreatedAt"); + + b.ToTable("WebhookLogs"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Attendances") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Branches") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.BranchMenuItemOverride", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("BranchOverrides") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("MenuItem"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeFeatureOverride", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", null) + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Reviews") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CashTransaction", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Shift", "Shift") + .WithMany("Transactions") + .HasForeignKey("ShiftId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Shift"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Coupons") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Customers") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DailyReport", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DeliveryCommissionRate", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Staff") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Employees") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Salaries") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSchedule", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Schedules") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Expense", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Shift", "Shift") + .WithMany() + .HasForeignKey("ShiftId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Shift"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Ingredients") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("LeaveRequests") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuCategories") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Tax", "Tax") + .WithMany("MenuCategories") + .HasForeignKey("TaxId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Tax"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuItems") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuCategory", "Category") + .WithMany("MenuItems") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItemIngredient", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Ingredient", "Ingredient") + .WithMany("MenuItemRecipes") + .HasForeignKey("IngredientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("RecipeIngredients") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Ingredient"); + + b.Navigation("MenuItem"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Orders") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Orders") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Coupon", "Coupon") + .WithMany("Orders") + .HasForeignKey("CouponId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Orders") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.TableReservation", "Reservation") + .WithMany() + .HasForeignKey("ReservationId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany("Orders") + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Coupon"); + + b.Navigation("Customer"); + + b.Navigation("Employee"); + + b.Navigation("Reservation"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("OrderItems") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Items") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MenuItem"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Payments") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.QueueTicket", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Employee", "ClosedBy") + .WithMany() + .HasForeignKey("ClosedByUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "OpenedBy") + .WithMany() + .HasForeignKey("OpenedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("ClosedBy"); + + b.Navigation("OpenedBy"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Ingredient", "Ingredient") + .WithMany("Movements") + .HasForeignKey("IngredientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Ingredient"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("SubscriptionPayments") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicket", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Employee", "CreatedByEmployee") + .WithMany() + .HasForeignKey("CreatedByEmployeeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("CreatedByEmployee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicketMessage", b => + { + b.HasOne("Meezi.Core.Entities.SupportTicket", "Ticket") + .WithMany("Messages") + .HasForeignKey("TicketId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ticket"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Tables") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Tables") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.TableSection", "Section") + .WithMany("Tables") + .HasForeignKey("SectionId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Section"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany() + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Customer"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Sections") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Taxes") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.WebhookLog", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Navigation("Orders"); + + b.Navigation("Sections"); + + b.Navigation("Staff"); + + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Navigation("Branches"); + + b.Navigation("Coupons"); + + b.Navigation("Customers"); + + b.Navigation("Employees"); + + b.Navigation("Ingredients"); + + b.Navigation("MenuCategories"); + + b.Navigation("MenuItems"); + + b.Navigation("Orders"); + + b.Navigation("Reviews"); + + b.Navigation("SubscriptionPayments"); + + b.Navigation("Tables"); + + b.Navigation("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Navigation("Attendances"); + + b.Navigation("LeaveRequests"); + + b.Navigation("Orders"); + + b.Navigation("Salaries"); + + b.Navigation("Schedules"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Navigation("MenuItemRecipes"); + + b.Navigation("Movements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Navigation("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Navigation("BranchOverrides"); + + b.Navigation("OrderItems"); + + b.Navigation("RecipeIngredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Navigation("Items"); + + b.Navigation("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicket", b => + { + b.Navigation("Messages"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Navigation("MenuCategories"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260522174742_MenuItemRecipesAndIngredientCost.cs b/src/Meezi.Infrastructure/Data/Migrations/20260522174742_MenuItemRecipesAndIngredientCost.cs new file mode 100644 index 0000000..a32cd7d --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260522174742_MenuItemRecipesAndIngredientCost.cs @@ -0,0 +1,153 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + /// + public partial class MenuItemRecipesAndIngredientCost : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_StockMovements_CafeId", + table: "StockMovements"); + + migrationBuilder.AddColumn( + name: "Kind", + table: "StockMovements", + type: "character varying(30)", + maxLength: 30, + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "OrderId", + table: "StockMovements", + type: "character varying(64)", + maxLength: 64, + nullable: true); + + migrationBuilder.AddColumn( + name: "LowStockWarningPercent", + table: "Ingredients", + type: "numeric(5,2)", + precision: 5, + scale: 2, + nullable: false, + defaultValue: 0m); + + migrationBuilder.AddColumn( + name: "ParLevel", + table: "Ingredients", + type: "numeric(18,3)", + precision: 18, + scale: 3, + nullable: false, + defaultValue: 0m); + + migrationBuilder.AddColumn( + name: "UnitCost", + table: "Ingredients", + type: "numeric(18,2)", + precision: 18, + scale: 2, + nullable: false, + defaultValue: 0m); + + migrationBuilder.CreateTable( + name: "MenuItemIngredients", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + MenuItemId = table.Column(type: "text", nullable: false), + IngredientId = table.Column(type: "text", nullable: false), + QuantityPerUnit = table.Column(type: "numeric(18,3)", precision: 18, scale: 3, nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CafeId = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_MenuItemIngredients", x => x.Id); + table.ForeignKey( + name: "FK_MenuItemIngredients_Cafes_CafeId", + column: x => x.CafeId, + principalTable: "Cafes", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_MenuItemIngredients_Ingredients_IngredientId", + column: x => x.IngredientId, + principalTable: "Ingredients", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_MenuItemIngredients_MenuItems_MenuItemId", + column: x => x.MenuItemId, + principalTable: "MenuItems", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_StockMovements_CafeId_OrderId", + table: "StockMovements", + columns: new[] { "CafeId", "OrderId" }); + + migrationBuilder.CreateIndex( + name: "IX_MenuItemIngredients_CafeId_MenuItemId_IngredientId", + table: "MenuItemIngredients", + columns: new[] { "CafeId", "MenuItemId", "IngredientId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_MenuItemIngredients_IngredientId", + table: "MenuItemIngredients", + column: "IngredientId"); + + migrationBuilder.CreateIndex( + name: "IX_MenuItemIngredients_MenuItemId", + table: "MenuItemIngredients", + column: "MenuItemId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "MenuItemIngredients"); + + migrationBuilder.DropIndex( + name: "IX_StockMovements_CafeId_OrderId", + table: "StockMovements"); + + migrationBuilder.DropColumn( + name: "Kind", + table: "StockMovements"); + + migrationBuilder.DropColumn( + name: "OrderId", + table: "StockMovements"); + + migrationBuilder.DropColumn( + name: "LowStockWarningPercent", + table: "Ingredients"); + + migrationBuilder.DropColumn( + name: "ParLevel", + table: "Ingredients"); + + migrationBuilder.DropColumn( + name: "UnitCost", + table: "Ingredients"); + + migrationBuilder.CreateIndex( + name: "IX_StockMovements_CafeId", + table: "StockMovements", + column: "CafeId"); + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260522183853_EmployeePhoneUniquePerCafe.Designer.cs b/src/Meezi.Infrastructure/Data/Migrations/20260522183853_EmployeePhoneUniquePerCafe.Designer.cs new file mode 100644 index 0000000..a3cef43 --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260522183853_EmployeePhoneUniquePerCafe.Designer.cs @@ -0,0 +1,2684 @@ +// +using System; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260522183853_EmployeePhoneUniquePerCafe")] + partial class EmployeePhoneUniquePerCafe + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ClockIn") + .HasColumnType("timestamp with time zone"); + + b.Property("ClockOut") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "Date") + .IsUnique(); + + b.ToTable("Attendances"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccentColor") + .HasColumnType("text"); + + b.Property("Address") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("AutoCutEnabled") + .HasColumnType("boolean"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("City") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("KitchenPrinterIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("KitchenPrinterPort") + .HasColumnType("integer"); + + b.Property("LogoUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("PaperWidthMm") + .HasColumnType("integer"); + + b.Property("Phone") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("PosDeviceIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("PosDevicePort") + .HasColumnType("integer"); + + b.Property("ReceiptFooter") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptHeader") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptPrinterIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("ReceiptPrinterPort") + .HasColumnType("integer"); + + b.Property("TaxRate") + .HasColumnType("numeric"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WelcomeText") + .HasColumnType("text"); + + b.Property("WifiPassword") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "IsActive"); + + b.ToTable("Branches"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.BranchMenuItemOverride", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("PriceOverride") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("SortOrderOverride") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedByUserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("BranchId", "MenuItemId") + .IsUnique(); + + b.ToTable("BranchMenuItemOverrides"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("AllowBranchTaxOverride") + .HasColumnType("boolean"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("CoverImageUrl") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DefaultTaxRate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DigikalaVendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("IsSuspended") + .HasColumnType("boolean"); + + b.Property("IsVerified") + .HasColumnType("boolean"); + + b.Property("LogoUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("PlanExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("PreferredLanguage") + .IsRequired() + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SnappfoodVendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Tap30VendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ThemeJson") + .HasMaxLength(8000) + .HasColumnType("character varying(8000)"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Cafes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeFeatureOverride", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FeatureKey") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "FeatureKey") + .IsUnique(); + + b.ToTable("CafeFeatureOverrides"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeNotification", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Body") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsRead") + .HasColumnType("boolean"); + + b.Property("ReadAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReferenceId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("TableNumber") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "IsRead", "CreatedAt"); + + b.ToTable("CafeNotifications"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AuthorName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("AuthorPhone") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Comment") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerRepliedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerReply") + .HasColumnType("text"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "CreatedAt"); + + b.ToTable("CafeReviews"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CashTransaction", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("ReferenceId") + .HasColumnType("text"); + + b.Property("ShiftId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("ShiftId"); + + b.HasIndex("CafeId", "BranchId"); + + b.ToTable("CashTransactions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MaxDiscount") + .HasColumnType("numeric"); + + b.Property("MinOrderAmount") + .HasColumnType("numeric"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TargetGroup") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UsageLimit") + .HasColumnType("integer"); + + b.Property("UsedCount") + .HasColumnType("integer"); + + b.Property("Value") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Code") + .IsUnique(); + + b.ToTable("Coupons"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BirthDateJalali") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Group") + .HasColumnType("integer"); + + b.Property("LoyaltyPoints") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReferredBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DailyReport", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AvgOrderValue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CardRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CashRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreditRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GeneratedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("NetIncome") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TopProducts") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("TotalExpenses") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TotalOrders") + .HasColumnType("integer"); + + b.Property("TotalRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TotalVoids") + .HasColumnType("integer"); + + b.Property("VoidAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "BranchId", "Date") + .IsUnique(); + + b.ToTable("DailyReports"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DeliveryCommissionRate", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Platform") + .HasColumnType("integer"); + + b.Property("RatePercent") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Platform") + .IsUnique(); + + b.ToTable("DeliveryCommissionRates"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("PinCode") + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "Phone") + .IsUnique() + .HasFilter("\"DeletedAt\" IS NULL"); + + b.ToTable("Employees"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Deductions") + .HasColumnType("numeric"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsPaid") + .HasColumnType("boolean"); + + b.Property("MonthYear") + .IsRequired() + .HasColumnType("text"); + + b.Property("NetSalary") + .HasColumnType("numeric"); + + b.Property("OvertimePay") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "MonthYear") + .IsUnique(); + + b.ToTable("EmployeeSalaries"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSchedule", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DayOfWeek") + .HasColumnType("integer"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShiftType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "DayOfWeek") + .IsUnique(); + + b.ToTable("EmployeeSchedules", (string)null); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Expense", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Category") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptImageUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ShiftId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("ShiftId"); + + b.HasIndex("CafeId", "BranchId", "CreatedAt"); + + b.ToTable("Expenses"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LowStockWarningPercent") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ParLevel") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("QuantityOnHand") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("ReorderLevel") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("Unit") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("UnitCost") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Ingredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndDate") + .HasColumnType("date"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("ReviewedBy") + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId"); + + b.ToTable("LeaveRequests"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("Icon") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("IconPresetId") + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("IconStyle") + .HasMaxLength(16) + .HasColumnType("character varying(16)"); + + b.Property("ImageUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("TaxId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("TaxId"); + + b.ToTable("MenuCategories"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CategoryId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("CategoryId"); + + b.ToTable("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItemIngredient", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IngredientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("QuantityPerUnit") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.HasKey("Id"); + + b.HasIndex("IngredientId"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("CafeId", "MenuItemId", "IngredientId") + .IsUnique(); + + b.ToTable("MenuItemIngredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CouponId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeliveryMetaJson") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("DeliveryPlatform") + .HasColumnType("integer"); + + b.Property("DiscountAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("EmployeeId") + .HasColumnType("text"); + + b.Property("ExternalOrderId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("GuestName") + .HasColumnType("text"); + + b.Property("GuestPhone") + .HasColumnType("text"); + + b.Property("GuestTrackingToken") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("OrderType") + .HasColumnType("integer"); + + b.Property("PlatformCommission") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ReservationId") + .HasColumnType("text"); + + b.Property("SnappfoodOrderId") + .HasColumnType("text"); + + b.Property("Source") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("StatusUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Subtotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("TaxTotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Total") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CouponId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("GuestTrackingToken"); + + b.HasIndex("ReservationId"); + + b.HasIndex("TableId"); + + b.HasIndex("CafeId", "DeliveryPlatform", "ExternalOrderId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsVoided") + .HasColumnType("boolean"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UnitPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VoidedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VoidedByUserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Reference") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.PlatformFeature", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DisplayNameEn") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DisplayNameFa") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("IsEnabledGlobally") + .HasColumnType("boolean"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("ModuleGroup") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("PlatformFeatures"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.PlatformPlanDefinition", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DisplayNameEn") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DisplayNameFa") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("FeaturesJson") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsBillableOnline") + .HasColumnType("boolean"); + + b.Property("LimitsJson") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("MonthlyPriceToman") + .HasPrecision(18) + .HasColumnType("numeric(18,0)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("Tier") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Tier") + .IsUnique(); + + b.ToTable("PlatformPlanDefinitions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.PlatformSetting", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DescriptionFa") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(8000) + .HasColumnType("character varying(8000)"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("PlatformSettings"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.QueueTicket", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerLabel") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedByUserId") + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("integer"); + + b.Property("OrderId") + .HasColumnType("text"); + + b.Property("ServiceDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("OrderId"); + + b.HasIndex("CafeId", "BranchId", "ServiceDate", "Number") + .IsUnique(); + + b.ToTable("QueueTickets"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClosedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ClosedByUserId") + .HasColumnType("text"); + + b.Property("ClosingCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Discrepancy") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ExpectedCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("OpenedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OpenedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("OpeningCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("ClosedByUserId"); + + b.HasIndex("OpenedByUserId"); + + b.HasIndex("BranchId", "Status"); + + b.ToTable("RegisterShifts", (string)null); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Delta") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("IngredientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Kind") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("OrderId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("IngredientId"); + + b.HasIndex("CafeId", "OrderId"); + + b.ToTable("StockMovements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AmountRials") + .HasColumnType("bigint"); + + b.Property("AmountToman") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Authority") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Months") + .HasColumnType("integer"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("RefId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Authority"); + + b.HasIndex("CafeId"); + + b.ToTable("SubscriptionPayments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicket", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AssignedAdminId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClosedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByEmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Priority") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subject") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedByEmployeeId"); + + b.HasIndex("CafeId", "Status", "UpdatedAt"); + + b.ToTable("SupportTickets"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicketMessage", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Body") + .IsRequired() + .HasMaxLength(8000) + .HasColumnType("character varying(8000)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SenderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("SenderKind") + .HasColumnType("integer"); + + b.Property("TicketId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("TicketId", "CreatedAt"); + + b.ToTable("SupportTicketMessages"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SystemAdmin", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.HasKey("Id"); + + b.HasIndex("Phone") + .IsUnique(); + + b.ToTable("SystemAdmins"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Floor") + .HasColumnType("text"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsCleaning") + .HasColumnType("boolean"); + + b.Property("Number") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("QrCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("SectionId") + .HasColumnType("text"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("QrCode") + .IsUnique(); + + b.HasIndex("SectionId"); + + b.HasIndex("BranchId", "SectionId", "SortOrder"); + + b.ToTable("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GuestName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GuestPhone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("PartySize") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("time without time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("TableId"); + + b.HasIndex("CafeId", "Date", "Time"); + + b.ToTable("TableReservations"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("BranchId", "Name"); + + b.ToTable("TableSections"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsCompound") + .HasColumnType("boolean"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("IsRequired") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.WebhookLog", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AttemptCount") + .HasColumnType("integer"); + + b.Property("CafeId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("ExternalOrderId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("MeeziOrderId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Platform") + .HasColumnType("integer"); + + b.Property("Processed") + .HasColumnType("boolean"); + + b.Property("ProcessedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RawBody") + .IsRequired() + .HasColumnType("text"); + + b.Property("SignatureHeader") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("SignatureValid") + .HasColumnType("boolean"); + + b.Property("Success") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("Platform", "CreatedAt"); + + b.ToTable("WebhookLogs"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Attendances") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Branches") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.BranchMenuItemOverride", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("BranchOverrides") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("MenuItem"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeFeatureOverride", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", null) + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Reviews") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CashTransaction", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Shift", "Shift") + .WithMany("Transactions") + .HasForeignKey("ShiftId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Shift"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Coupons") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Customers") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DailyReport", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DeliveryCommissionRate", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Staff") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Employees") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Salaries") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSchedule", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Schedules") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Expense", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Shift", "Shift") + .WithMany() + .HasForeignKey("ShiftId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Shift"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Ingredients") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("LeaveRequests") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuCategories") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Tax", "Tax") + .WithMany("MenuCategories") + .HasForeignKey("TaxId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Tax"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuItems") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuCategory", "Category") + .WithMany("MenuItems") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItemIngredient", b => + { + b.HasOne("Meezi.Core.Entities.Ingredient", "Ingredient") + .WithMany("MenuItemRecipes") + .HasForeignKey("IngredientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("RecipeIngredients") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ingredient"); + + b.Navigation("MenuItem"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Orders") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Orders") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Coupon", "Coupon") + .WithMany("Orders") + .HasForeignKey("CouponId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Orders") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.TableReservation", "Reservation") + .WithMany() + .HasForeignKey("ReservationId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany("Orders") + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Coupon"); + + b.Navigation("Customer"); + + b.Navigation("Employee"); + + b.Navigation("Reservation"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("OrderItems") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Items") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MenuItem"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Payments") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.QueueTicket", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Employee", "ClosedBy") + .WithMany() + .HasForeignKey("ClosedByUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "OpenedBy") + .WithMany() + .HasForeignKey("OpenedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("ClosedBy"); + + b.Navigation("OpenedBy"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Ingredient", "Ingredient") + .WithMany("Movements") + .HasForeignKey("IngredientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Ingredient"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("SubscriptionPayments") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicket", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Employee", "CreatedByEmployee") + .WithMany() + .HasForeignKey("CreatedByEmployeeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("CreatedByEmployee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicketMessage", b => + { + b.HasOne("Meezi.Core.Entities.SupportTicket", "Ticket") + .WithMany("Messages") + .HasForeignKey("TicketId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ticket"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Tables") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Tables") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.TableSection", "Section") + .WithMany("Tables") + .HasForeignKey("SectionId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Section"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany() + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Customer"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Sections") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Taxes") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.WebhookLog", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Navigation("Orders"); + + b.Navigation("Sections"); + + b.Navigation("Staff"); + + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Navigation("Branches"); + + b.Navigation("Coupons"); + + b.Navigation("Customers"); + + b.Navigation("Employees"); + + b.Navigation("Ingredients"); + + b.Navigation("MenuCategories"); + + b.Navigation("MenuItems"); + + b.Navigation("Orders"); + + b.Navigation("Reviews"); + + b.Navigation("SubscriptionPayments"); + + b.Navigation("Tables"); + + b.Navigation("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Navigation("Attendances"); + + b.Navigation("LeaveRequests"); + + b.Navigation("Orders"); + + b.Navigation("Salaries"); + + b.Navigation("Schedules"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Navigation("MenuItemRecipes"); + + b.Navigation("Movements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Navigation("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Navigation("BranchOverrides"); + + b.Navigation("OrderItems"); + + b.Navigation("RecipeIngredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Navigation("Items"); + + b.Navigation("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicket", b => + { + b.Navigation("Messages"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Navigation("MenuCategories"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260522183853_EmployeePhoneUniquePerCafe.cs b/src/Meezi.Infrastructure/Data/Migrations/20260522183853_EmployeePhoneUniquePerCafe.cs new file mode 100644 index 0000000..f7a442f --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260522183853_EmployeePhoneUniquePerCafe.cs @@ -0,0 +1,50 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + /// + public partial class EmployeePhoneUniquePerCafe : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_MenuItemIngredients_Cafes_CafeId", + table: "MenuItemIngredients"); + + migrationBuilder.DropIndex( + name: "IX_Employees_CafeId_Phone", + table: "Employees"); + + migrationBuilder.CreateIndex( + name: "IX_Employees_CafeId_Phone", + table: "Employees", + columns: new[] { "CafeId", "Phone" }, + unique: true, + filter: "\"DeletedAt\" IS NULL"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_Employees_CafeId_Phone", + table: "Employees"); + + migrationBuilder.CreateIndex( + name: "IX_Employees_CafeId_Phone", + table: "Employees", + columns: new[] { "CafeId", "Phone" }); + + migrationBuilder.AddForeignKey( + name: "FK_MenuItemIngredients_Cafes_CafeId", + table: "MenuItemIngredients", + column: "CafeId", + principalTable: "Cafes", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260522185144_AddMenuItemModel3d.Designer.cs b/src/Meezi.Infrastructure/Data/Migrations/20260522185144_AddMenuItemModel3d.Designer.cs new file mode 100644 index 0000000..c129482 --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260522185144_AddMenuItemModel3d.Designer.cs @@ -0,0 +1,2688 @@ +// +using System; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260522185144_AddMenuItemModel3d")] + partial class AddMenuItemModel3d + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ClockIn") + .HasColumnType("timestamp with time zone"); + + b.Property("ClockOut") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "Date") + .IsUnique(); + + b.ToTable("Attendances"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccentColor") + .HasColumnType("text"); + + b.Property("Address") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("AutoCutEnabled") + .HasColumnType("boolean"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("City") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("KitchenPrinterIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("KitchenPrinterPort") + .HasColumnType("integer"); + + b.Property("LogoUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("PaperWidthMm") + .HasColumnType("integer"); + + b.Property("Phone") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("PosDeviceIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("PosDevicePort") + .HasColumnType("integer"); + + b.Property("ReceiptFooter") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptHeader") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptPrinterIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("ReceiptPrinterPort") + .HasColumnType("integer"); + + b.Property("TaxRate") + .HasColumnType("numeric"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WelcomeText") + .HasColumnType("text"); + + b.Property("WifiPassword") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "IsActive"); + + b.ToTable("Branches"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.BranchMenuItemOverride", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("PriceOverride") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("SortOrderOverride") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedByUserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("BranchId", "MenuItemId") + .IsUnique(); + + b.ToTable("BranchMenuItemOverrides"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("AllowBranchTaxOverride") + .HasColumnType("boolean"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("CoverImageUrl") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DefaultTaxRate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DigikalaVendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("IsSuspended") + .HasColumnType("boolean"); + + b.Property("IsVerified") + .HasColumnType("boolean"); + + b.Property("LogoUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("PlanExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("PreferredLanguage") + .IsRequired() + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SnappfoodVendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Tap30VendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ThemeJson") + .HasMaxLength(8000) + .HasColumnType("character varying(8000)"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Cafes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeFeatureOverride", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FeatureKey") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "FeatureKey") + .IsUnique(); + + b.ToTable("CafeFeatureOverrides"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeNotification", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Body") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsRead") + .HasColumnType("boolean"); + + b.Property("ReadAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReferenceId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("TableNumber") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "IsRead", "CreatedAt"); + + b.ToTable("CafeNotifications"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AuthorName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("AuthorPhone") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Comment") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerRepliedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerReply") + .HasColumnType("text"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "CreatedAt"); + + b.ToTable("CafeReviews"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CashTransaction", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("ReferenceId") + .HasColumnType("text"); + + b.Property("ShiftId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("ShiftId"); + + b.HasIndex("CafeId", "BranchId"); + + b.ToTable("CashTransactions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MaxDiscount") + .HasColumnType("numeric"); + + b.Property("MinOrderAmount") + .HasColumnType("numeric"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TargetGroup") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UsageLimit") + .HasColumnType("integer"); + + b.Property("UsedCount") + .HasColumnType("integer"); + + b.Property("Value") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Code") + .IsUnique(); + + b.ToTable("Coupons"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BirthDateJalali") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Group") + .HasColumnType("integer"); + + b.Property("LoyaltyPoints") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReferredBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DailyReport", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AvgOrderValue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CardRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CashRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreditRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GeneratedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("NetIncome") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TopProducts") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("TotalExpenses") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TotalOrders") + .HasColumnType("integer"); + + b.Property("TotalRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TotalVoids") + .HasColumnType("integer"); + + b.Property("VoidAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "BranchId", "Date") + .IsUnique(); + + b.ToTable("DailyReports"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DeliveryCommissionRate", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Platform") + .HasColumnType("integer"); + + b.Property("RatePercent") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Platform") + .IsUnique(); + + b.ToTable("DeliveryCommissionRates"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("PinCode") + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "Phone") + .IsUnique() + .HasFilter("\"DeletedAt\" IS NULL"); + + b.ToTable("Employees"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Deductions") + .HasColumnType("numeric"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsPaid") + .HasColumnType("boolean"); + + b.Property("MonthYear") + .IsRequired() + .HasColumnType("text"); + + b.Property("NetSalary") + .HasColumnType("numeric"); + + b.Property("OvertimePay") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "MonthYear") + .IsUnique(); + + b.ToTable("EmployeeSalaries"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSchedule", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DayOfWeek") + .HasColumnType("integer"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShiftType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "DayOfWeek") + .IsUnique(); + + b.ToTable("EmployeeSchedules", (string)null); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Expense", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Category") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptImageUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ShiftId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("ShiftId"); + + b.HasIndex("CafeId", "BranchId", "CreatedAt"); + + b.ToTable("Expenses"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LowStockWarningPercent") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ParLevel") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("QuantityOnHand") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("ReorderLevel") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("Unit") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("UnitCost") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Ingredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndDate") + .HasColumnType("date"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("ReviewedBy") + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId"); + + b.ToTable("LeaveRequests"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("Icon") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("IconPresetId") + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("IconStyle") + .HasMaxLength(16) + .HasColumnType("character varying(16)"); + + b.Property("ImageUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("TaxId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("TaxId"); + + b.ToTable("MenuCategories"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CategoryId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Model3dUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("CategoryId"); + + b.ToTable("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItemIngredient", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IngredientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("QuantityPerUnit") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.HasKey("Id"); + + b.HasIndex("IngredientId"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("CafeId", "MenuItemId", "IngredientId") + .IsUnique(); + + b.ToTable("MenuItemIngredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CouponId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeliveryMetaJson") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("DeliveryPlatform") + .HasColumnType("integer"); + + b.Property("DiscountAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("EmployeeId") + .HasColumnType("text"); + + b.Property("ExternalOrderId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("GuestName") + .HasColumnType("text"); + + b.Property("GuestPhone") + .HasColumnType("text"); + + b.Property("GuestTrackingToken") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("OrderType") + .HasColumnType("integer"); + + b.Property("PlatformCommission") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ReservationId") + .HasColumnType("text"); + + b.Property("SnappfoodOrderId") + .HasColumnType("text"); + + b.Property("Source") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("StatusUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Subtotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("TaxTotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Total") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CouponId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("GuestTrackingToken"); + + b.HasIndex("ReservationId"); + + b.HasIndex("TableId"); + + b.HasIndex("CafeId", "DeliveryPlatform", "ExternalOrderId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsVoided") + .HasColumnType("boolean"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UnitPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VoidedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VoidedByUserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Reference") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.PlatformFeature", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DisplayNameEn") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DisplayNameFa") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("IsEnabledGlobally") + .HasColumnType("boolean"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("ModuleGroup") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("PlatformFeatures"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.PlatformPlanDefinition", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DisplayNameEn") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DisplayNameFa") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("FeaturesJson") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsBillableOnline") + .HasColumnType("boolean"); + + b.Property("LimitsJson") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("MonthlyPriceToman") + .HasPrecision(18) + .HasColumnType("numeric(18,0)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("Tier") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Tier") + .IsUnique(); + + b.ToTable("PlatformPlanDefinitions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.PlatformSetting", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DescriptionFa") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(8000) + .HasColumnType("character varying(8000)"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("PlatformSettings"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.QueueTicket", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerLabel") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedByUserId") + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("integer"); + + b.Property("OrderId") + .HasColumnType("text"); + + b.Property("ServiceDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("OrderId"); + + b.HasIndex("CafeId", "BranchId", "ServiceDate", "Number") + .IsUnique(); + + b.ToTable("QueueTickets"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClosedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ClosedByUserId") + .HasColumnType("text"); + + b.Property("ClosingCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Discrepancy") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ExpectedCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("OpenedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OpenedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("OpeningCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("ClosedByUserId"); + + b.HasIndex("OpenedByUserId"); + + b.HasIndex("BranchId", "Status"); + + b.ToTable("RegisterShifts", (string)null); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Delta") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("IngredientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Kind") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("OrderId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("IngredientId"); + + b.HasIndex("CafeId", "OrderId"); + + b.ToTable("StockMovements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AmountRials") + .HasColumnType("bigint"); + + b.Property("AmountToman") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Authority") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Months") + .HasColumnType("integer"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("RefId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Authority"); + + b.HasIndex("CafeId"); + + b.ToTable("SubscriptionPayments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicket", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AssignedAdminId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClosedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByEmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Priority") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subject") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedByEmployeeId"); + + b.HasIndex("CafeId", "Status", "UpdatedAt"); + + b.ToTable("SupportTickets"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicketMessage", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Body") + .IsRequired() + .HasMaxLength(8000) + .HasColumnType("character varying(8000)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SenderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("SenderKind") + .HasColumnType("integer"); + + b.Property("TicketId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("TicketId", "CreatedAt"); + + b.ToTable("SupportTicketMessages"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SystemAdmin", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.HasKey("Id"); + + b.HasIndex("Phone") + .IsUnique(); + + b.ToTable("SystemAdmins"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Floor") + .HasColumnType("text"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsCleaning") + .HasColumnType("boolean"); + + b.Property("Number") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("QrCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("SectionId") + .HasColumnType("text"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("QrCode") + .IsUnique(); + + b.HasIndex("SectionId"); + + b.HasIndex("BranchId", "SectionId", "SortOrder"); + + b.ToTable("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GuestName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GuestPhone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("PartySize") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("time without time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("TableId"); + + b.HasIndex("CafeId", "Date", "Time"); + + b.ToTable("TableReservations"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("BranchId", "Name"); + + b.ToTable("TableSections"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsCompound") + .HasColumnType("boolean"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("IsRequired") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.WebhookLog", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AttemptCount") + .HasColumnType("integer"); + + b.Property("CafeId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("ExternalOrderId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("MeeziOrderId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Platform") + .HasColumnType("integer"); + + b.Property("Processed") + .HasColumnType("boolean"); + + b.Property("ProcessedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RawBody") + .IsRequired() + .HasColumnType("text"); + + b.Property("SignatureHeader") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("SignatureValid") + .HasColumnType("boolean"); + + b.Property("Success") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("Platform", "CreatedAt"); + + b.ToTable("WebhookLogs"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Attendances") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Branches") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.BranchMenuItemOverride", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("BranchOverrides") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("MenuItem"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeFeatureOverride", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", null) + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Reviews") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CashTransaction", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Shift", "Shift") + .WithMany("Transactions") + .HasForeignKey("ShiftId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Shift"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Coupons") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Customers") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DailyReport", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DeliveryCommissionRate", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Staff") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Employees") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Salaries") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSchedule", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Schedules") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Expense", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Shift", "Shift") + .WithMany() + .HasForeignKey("ShiftId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Shift"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Ingredients") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("LeaveRequests") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuCategories") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Tax", "Tax") + .WithMany("MenuCategories") + .HasForeignKey("TaxId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Tax"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuItems") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuCategory", "Category") + .WithMany("MenuItems") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItemIngredient", b => + { + b.HasOne("Meezi.Core.Entities.Ingredient", "Ingredient") + .WithMany("MenuItemRecipes") + .HasForeignKey("IngredientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("RecipeIngredients") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ingredient"); + + b.Navigation("MenuItem"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Orders") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Orders") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Coupon", "Coupon") + .WithMany("Orders") + .HasForeignKey("CouponId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Orders") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.TableReservation", "Reservation") + .WithMany() + .HasForeignKey("ReservationId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany("Orders") + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Coupon"); + + b.Navigation("Customer"); + + b.Navigation("Employee"); + + b.Navigation("Reservation"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("OrderItems") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Items") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MenuItem"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Payments") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.QueueTicket", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Employee", "ClosedBy") + .WithMany() + .HasForeignKey("ClosedByUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "OpenedBy") + .WithMany() + .HasForeignKey("OpenedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("ClosedBy"); + + b.Navigation("OpenedBy"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Ingredient", "Ingredient") + .WithMany("Movements") + .HasForeignKey("IngredientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Ingredient"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("SubscriptionPayments") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicket", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Employee", "CreatedByEmployee") + .WithMany() + .HasForeignKey("CreatedByEmployeeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("CreatedByEmployee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicketMessage", b => + { + b.HasOne("Meezi.Core.Entities.SupportTicket", "Ticket") + .WithMany("Messages") + .HasForeignKey("TicketId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ticket"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Tables") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Tables") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.TableSection", "Section") + .WithMany("Tables") + .HasForeignKey("SectionId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Section"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany() + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Customer"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Sections") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Taxes") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.WebhookLog", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Navigation("Orders"); + + b.Navigation("Sections"); + + b.Navigation("Staff"); + + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Navigation("Branches"); + + b.Navigation("Coupons"); + + b.Navigation("Customers"); + + b.Navigation("Employees"); + + b.Navigation("Ingredients"); + + b.Navigation("MenuCategories"); + + b.Navigation("MenuItems"); + + b.Navigation("Orders"); + + b.Navigation("Reviews"); + + b.Navigation("SubscriptionPayments"); + + b.Navigation("Tables"); + + b.Navigation("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Navigation("Attendances"); + + b.Navigation("LeaveRequests"); + + b.Navigation("Orders"); + + b.Navigation("Salaries"); + + b.Navigation("Schedules"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Navigation("MenuItemRecipes"); + + b.Navigation("Movements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Navigation("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Navigation("BranchOverrides"); + + b.Navigation("OrderItems"); + + b.Navigation("RecipeIngredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Navigation("Items"); + + b.Navigation("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicket", b => + { + b.Navigation("Messages"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Navigation("MenuCategories"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260522185144_AddMenuItemModel3d.cs b/src/Meezi.Infrastructure/Data/Migrations/20260522185144_AddMenuItemModel3d.cs new file mode 100644 index 0000000..7a12873 --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260522185144_AddMenuItemModel3d.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + /// + public partial class AddMenuItemModel3d : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Model3dUrl", + table: "MenuItems", + type: "character varying(500)", + maxLength: 500, + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Model3dUrl", + table: "MenuItems"); + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260522190504_AddSubscriptionPaymentProvider.Designer.cs b/src/Meezi.Infrastructure/Data/Migrations/20260522190504_AddSubscriptionPaymentProvider.Designer.cs new file mode 100644 index 0000000..0a54a5a --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260522190504_AddSubscriptionPaymentProvider.Designer.cs @@ -0,0 +1,2691 @@ +// +using System; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260522190504_AddSubscriptionPaymentProvider")] + partial class AddSubscriptionPaymentProvider + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ClockIn") + .HasColumnType("timestamp with time zone"); + + b.Property("ClockOut") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "Date") + .IsUnique(); + + b.ToTable("Attendances"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccentColor") + .HasColumnType("text"); + + b.Property("Address") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("AutoCutEnabled") + .HasColumnType("boolean"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("City") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("KitchenPrinterIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("KitchenPrinterPort") + .HasColumnType("integer"); + + b.Property("LogoUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("PaperWidthMm") + .HasColumnType("integer"); + + b.Property("Phone") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("PosDeviceIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("PosDevicePort") + .HasColumnType("integer"); + + b.Property("ReceiptFooter") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptHeader") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptPrinterIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("ReceiptPrinterPort") + .HasColumnType("integer"); + + b.Property("TaxRate") + .HasColumnType("numeric"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WelcomeText") + .HasColumnType("text"); + + b.Property("WifiPassword") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "IsActive"); + + b.ToTable("Branches"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.BranchMenuItemOverride", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("PriceOverride") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("SortOrderOverride") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedByUserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("BranchId", "MenuItemId") + .IsUnique(); + + b.ToTable("BranchMenuItemOverrides"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("AllowBranchTaxOverride") + .HasColumnType("boolean"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("CoverImageUrl") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DefaultTaxRate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DigikalaVendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("IsSuspended") + .HasColumnType("boolean"); + + b.Property("IsVerified") + .HasColumnType("boolean"); + + b.Property("LogoUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("PlanExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("PreferredLanguage") + .IsRequired() + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SnappfoodVendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Tap30VendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ThemeJson") + .HasMaxLength(8000) + .HasColumnType("character varying(8000)"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Cafes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeFeatureOverride", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FeatureKey") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "FeatureKey") + .IsUnique(); + + b.ToTable("CafeFeatureOverrides"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeNotification", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Body") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsRead") + .HasColumnType("boolean"); + + b.Property("ReadAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReferenceId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("TableNumber") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "IsRead", "CreatedAt"); + + b.ToTable("CafeNotifications"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AuthorName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("AuthorPhone") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Comment") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerRepliedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerReply") + .HasColumnType("text"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "CreatedAt"); + + b.ToTable("CafeReviews"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CashTransaction", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("ReferenceId") + .HasColumnType("text"); + + b.Property("ShiftId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("ShiftId"); + + b.HasIndex("CafeId", "BranchId"); + + b.ToTable("CashTransactions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MaxDiscount") + .HasColumnType("numeric"); + + b.Property("MinOrderAmount") + .HasColumnType("numeric"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TargetGroup") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UsageLimit") + .HasColumnType("integer"); + + b.Property("UsedCount") + .HasColumnType("integer"); + + b.Property("Value") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Code") + .IsUnique(); + + b.ToTable("Coupons"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BirthDateJalali") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Group") + .HasColumnType("integer"); + + b.Property("LoyaltyPoints") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReferredBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DailyReport", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AvgOrderValue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CardRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CashRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreditRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GeneratedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("NetIncome") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TopProducts") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("TotalExpenses") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TotalOrders") + .HasColumnType("integer"); + + b.Property("TotalRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TotalVoids") + .HasColumnType("integer"); + + b.Property("VoidAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "BranchId", "Date") + .IsUnique(); + + b.ToTable("DailyReports"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DeliveryCommissionRate", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Platform") + .HasColumnType("integer"); + + b.Property("RatePercent") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Platform") + .IsUnique(); + + b.ToTable("DeliveryCommissionRates"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("PinCode") + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "Phone") + .IsUnique() + .HasFilter("\"DeletedAt\" IS NULL"); + + b.ToTable("Employees"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Deductions") + .HasColumnType("numeric"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsPaid") + .HasColumnType("boolean"); + + b.Property("MonthYear") + .IsRequired() + .HasColumnType("text"); + + b.Property("NetSalary") + .HasColumnType("numeric"); + + b.Property("OvertimePay") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "MonthYear") + .IsUnique(); + + b.ToTable("EmployeeSalaries"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSchedule", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DayOfWeek") + .HasColumnType("integer"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShiftType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "DayOfWeek") + .IsUnique(); + + b.ToTable("EmployeeSchedules", (string)null); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Expense", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Category") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptImageUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ShiftId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("ShiftId"); + + b.HasIndex("CafeId", "BranchId", "CreatedAt"); + + b.ToTable("Expenses"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LowStockWarningPercent") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ParLevel") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("QuantityOnHand") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("ReorderLevel") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("Unit") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("UnitCost") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Ingredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndDate") + .HasColumnType("date"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("ReviewedBy") + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId"); + + b.ToTable("LeaveRequests"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("Icon") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("IconPresetId") + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("IconStyle") + .HasMaxLength(16) + .HasColumnType("character varying(16)"); + + b.Property("ImageUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("TaxId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("TaxId"); + + b.ToTable("MenuCategories"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CategoryId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Model3dUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("CategoryId"); + + b.ToTable("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItemIngredient", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IngredientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("QuantityPerUnit") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.HasKey("Id"); + + b.HasIndex("IngredientId"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("CafeId", "MenuItemId", "IngredientId") + .IsUnique(); + + b.ToTable("MenuItemIngredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CouponId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeliveryMetaJson") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("DeliveryPlatform") + .HasColumnType("integer"); + + b.Property("DiscountAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("EmployeeId") + .HasColumnType("text"); + + b.Property("ExternalOrderId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("GuestName") + .HasColumnType("text"); + + b.Property("GuestPhone") + .HasColumnType("text"); + + b.Property("GuestTrackingToken") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("OrderType") + .HasColumnType("integer"); + + b.Property("PlatformCommission") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ReservationId") + .HasColumnType("text"); + + b.Property("SnappfoodOrderId") + .HasColumnType("text"); + + b.Property("Source") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("StatusUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Subtotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("TaxTotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Total") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CouponId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("GuestTrackingToken"); + + b.HasIndex("ReservationId"); + + b.HasIndex("TableId"); + + b.HasIndex("CafeId", "DeliveryPlatform", "ExternalOrderId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsVoided") + .HasColumnType("boolean"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UnitPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VoidedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VoidedByUserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Reference") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.PlatformFeature", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DisplayNameEn") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DisplayNameFa") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("IsEnabledGlobally") + .HasColumnType("boolean"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("ModuleGroup") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("PlatformFeatures"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.PlatformPlanDefinition", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DisplayNameEn") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DisplayNameFa") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("FeaturesJson") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsBillableOnline") + .HasColumnType("boolean"); + + b.Property("LimitsJson") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("MonthlyPriceToman") + .HasPrecision(18) + .HasColumnType("numeric(18,0)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("Tier") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Tier") + .IsUnique(); + + b.ToTable("PlatformPlanDefinitions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.PlatformSetting", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DescriptionFa") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(8000) + .HasColumnType("character varying(8000)"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("PlatformSettings"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.QueueTicket", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerLabel") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedByUserId") + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("integer"); + + b.Property("OrderId") + .HasColumnType("text"); + + b.Property("ServiceDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("OrderId"); + + b.HasIndex("CafeId", "BranchId", "ServiceDate", "Number") + .IsUnique(); + + b.ToTable("QueueTickets"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClosedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ClosedByUserId") + .HasColumnType("text"); + + b.Property("ClosingCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Discrepancy") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ExpectedCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("OpenedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OpenedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("OpeningCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("ClosedByUserId"); + + b.HasIndex("OpenedByUserId"); + + b.HasIndex("BranchId", "Status"); + + b.ToTable("RegisterShifts", (string)null); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Delta") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("IngredientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Kind") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("OrderId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("IngredientId"); + + b.HasIndex("CafeId", "OrderId"); + + b.ToTable("StockMovements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AmountRials") + .HasColumnType("bigint"); + + b.Property("AmountToman") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Authority") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Months") + .HasColumnType("integer"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("Provider") + .HasColumnType("integer"); + + b.Property("RefId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Authority"); + + b.HasIndex("CafeId"); + + b.ToTable("SubscriptionPayments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicket", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AssignedAdminId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClosedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByEmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Priority") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subject") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedByEmployeeId"); + + b.HasIndex("CafeId", "Status", "UpdatedAt"); + + b.ToTable("SupportTickets"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicketMessage", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Body") + .IsRequired() + .HasMaxLength(8000) + .HasColumnType("character varying(8000)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SenderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("SenderKind") + .HasColumnType("integer"); + + b.Property("TicketId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("TicketId", "CreatedAt"); + + b.ToTable("SupportTicketMessages"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SystemAdmin", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.HasKey("Id"); + + b.HasIndex("Phone") + .IsUnique(); + + b.ToTable("SystemAdmins"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Floor") + .HasColumnType("text"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsCleaning") + .HasColumnType("boolean"); + + b.Property("Number") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("QrCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("SectionId") + .HasColumnType("text"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("QrCode") + .IsUnique(); + + b.HasIndex("SectionId"); + + b.HasIndex("BranchId", "SectionId", "SortOrder"); + + b.ToTable("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GuestName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GuestPhone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("PartySize") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("time without time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("TableId"); + + b.HasIndex("CafeId", "Date", "Time"); + + b.ToTable("TableReservations"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("BranchId", "Name"); + + b.ToTable("TableSections"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsCompound") + .HasColumnType("boolean"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("IsRequired") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.WebhookLog", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AttemptCount") + .HasColumnType("integer"); + + b.Property("CafeId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("ExternalOrderId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("MeeziOrderId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Platform") + .HasColumnType("integer"); + + b.Property("Processed") + .HasColumnType("boolean"); + + b.Property("ProcessedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RawBody") + .IsRequired() + .HasColumnType("text"); + + b.Property("SignatureHeader") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("SignatureValid") + .HasColumnType("boolean"); + + b.Property("Success") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("Platform", "CreatedAt"); + + b.ToTable("WebhookLogs"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Attendances") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Branches") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.BranchMenuItemOverride", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("BranchOverrides") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("MenuItem"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeFeatureOverride", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", null) + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Reviews") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CashTransaction", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Shift", "Shift") + .WithMany("Transactions") + .HasForeignKey("ShiftId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Shift"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Coupons") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Customers") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DailyReport", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DeliveryCommissionRate", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Staff") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Employees") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Salaries") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSchedule", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Schedules") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Expense", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Shift", "Shift") + .WithMany() + .HasForeignKey("ShiftId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Shift"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Ingredients") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("LeaveRequests") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuCategories") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Tax", "Tax") + .WithMany("MenuCategories") + .HasForeignKey("TaxId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Tax"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuItems") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuCategory", "Category") + .WithMany("MenuItems") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItemIngredient", b => + { + b.HasOne("Meezi.Core.Entities.Ingredient", "Ingredient") + .WithMany("MenuItemRecipes") + .HasForeignKey("IngredientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("RecipeIngredients") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ingredient"); + + b.Navigation("MenuItem"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Orders") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Orders") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Coupon", "Coupon") + .WithMany("Orders") + .HasForeignKey("CouponId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Orders") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.TableReservation", "Reservation") + .WithMany() + .HasForeignKey("ReservationId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany("Orders") + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Coupon"); + + b.Navigation("Customer"); + + b.Navigation("Employee"); + + b.Navigation("Reservation"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("OrderItems") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Items") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MenuItem"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Payments") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.QueueTicket", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Employee", "ClosedBy") + .WithMany() + .HasForeignKey("ClosedByUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "OpenedBy") + .WithMany() + .HasForeignKey("OpenedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("ClosedBy"); + + b.Navigation("OpenedBy"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Ingredient", "Ingredient") + .WithMany("Movements") + .HasForeignKey("IngredientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Ingredient"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("SubscriptionPayments") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicket", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Employee", "CreatedByEmployee") + .WithMany() + .HasForeignKey("CreatedByEmployeeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("CreatedByEmployee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicketMessage", b => + { + b.HasOne("Meezi.Core.Entities.SupportTicket", "Ticket") + .WithMany("Messages") + .HasForeignKey("TicketId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ticket"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Tables") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Tables") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.TableSection", "Section") + .WithMany("Tables") + .HasForeignKey("SectionId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Section"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany() + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Customer"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Sections") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Taxes") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.WebhookLog", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Navigation("Orders"); + + b.Navigation("Sections"); + + b.Navigation("Staff"); + + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Navigation("Branches"); + + b.Navigation("Coupons"); + + b.Navigation("Customers"); + + b.Navigation("Employees"); + + b.Navigation("Ingredients"); + + b.Navigation("MenuCategories"); + + b.Navigation("MenuItems"); + + b.Navigation("Orders"); + + b.Navigation("Reviews"); + + b.Navigation("SubscriptionPayments"); + + b.Navigation("Tables"); + + b.Navigation("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Navigation("Attendances"); + + b.Navigation("LeaveRequests"); + + b.Navigation("Orders"); + + b.Navigation("Salaries"); + + b.Navigation("Schedules"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Navigation("MenuItemRecipes"); + + b.Navigation("Movements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Navigation("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Navigation("BranchOverrides"); + + b.Navigation("OrderItems"); + + b.Navigation("RecipeIngredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Navigation("Items"); + + b.Navigation("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicket", b => + { + b.Navigation("Messages"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Navigation("MenuCategories"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260522190504_AddSubscriptionPaymentProvider.cs b/src/Meezi.Infrastructure/Data/Migrations/20260522190504_AddSubscriptionPaymentProvider.cs new file mode 100644 index 0000000..0c92f38 --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260522190504_AddSubscriptionPaymentProvider.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + /// + public partial class AddSubscriptionPaymentProvider : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Provider", + table: "SubscriptionPayments", + type: "integer", + nullable: false, + defaultValue: 0); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Provider", + table: "SubscriptionPayments"); + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260522195153_AddCafeDiscoverProfileJson.Designer.cs b/src/Meezi.Infrastructure/Data/Migrations/20260522195153_AddCafeDiscoverProfileJson.Designer.cs new file mode 100644 index 0000000..7235a02 --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260522195153_AddCafeDiscoverProfileJson.Designer.cs @@ -0,0 +1,2695 @@ +// +using System; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260522195153_AddCafeDiscoverProfileJson")] + partial class AddCafeDiscoverProfileJson + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ClockIn") + .HasColumnType("timestamp with time zone"); + + b.Property("ClockOut") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "Date") + .IsUnique(); + + b.ToTable("Attendances"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccentColor") + .HasColumnType("text"); + + b.Property("Address") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("AutoCutEnabled") + .HasColumnType("boolean"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("City") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("KitchenPrinterIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("KitchenPrinterPort") + .HasColumnType("integer"); + + b.Property("LogoUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("PaperWidthMm") + .HasColumnType("integer"); + + b.Property("Phone") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("PosDeviceIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("PosDevicePort") + .HasColumnType("integer"); + + b.Property("ReceiptFooter") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptHeader") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptPrinterIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("ReceiptPrinterPort") + .HasColumnType("integer"); + + b.Property("TaxRate") + .HasColumnType("numeric"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WelcomeText") + .HasColumnType("text"); + + b.Property("WifiPassword") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "IsActive"); + + b.ToTable("Branches"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.BranchMenuItemOverride", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("PriceOverride") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("SortOrderOverride") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedByUserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("BranchId", "MenuItemId") + .IsUnique(); + + b.ToTable("BranchMenuItemOverrides"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("AllowBranchTaxOverride") + .HasColumnType("boolean"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("CoverImageUrl") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DefaultTaxRate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DigikalaVendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("DiscoverProfileJson") + .HasMaxLength(8000) + .HasColumnType("character varying(8000)"); + + b.Property("IsSuspended") + .HasColumnType("boolean"); + + b.Property("IsVerified") + .HasColumnType("boolean"); + + b.Property("LogoUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("PlanExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("PreferredLanguage") + .IsRequired() + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SnappfoodVendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Tap30VendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ThemeJson") + .HasMaxLength(8000) + .HasColumnType("character varying(8000)"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Cafes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeFeatureOverride", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FeatureKey") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "FeatureKey") + .IsUnique(); + + b.ToTable("CafeFeatureOverrides"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeNotification", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Body") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsRead") + .HasColumnType("boolean"); + + b.Property("ReadAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReferenceId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("TableNumber") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "IsRead", "CreatedAt"); + + b.ToTable("CafeNotifications"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AuthorName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("AuthorPhone") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Comment") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerRepliedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerReply") + .HasColumnType("text"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "CreatedAt"); + + b.ToTable("CafeReviews"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CashTransaction", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("ReferenceId") + .HasColumnType("text"); + + b.Property("ShiftId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("ShiftId"); + + b.HasIndex("CafeId", "BranchId"); + + b.ToTable("CashTransactions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MaxDiscount") + .HasColumnType("numeric"); + + b.Property("MinOrderAmount") + .HasColumnType("numeric"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TargetGroup") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UsageLimit") + .HasColumnType("integer"); + + b.Property("UsedCount") + .HasColumnType("integer"); + + b.Property("Value") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Code") + .IsUnique(); + + b.ToTable("Coupons"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BirthDateJalali") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Group") + .HasColumnType("integer"); + + b.Property("LoyaltyPoints") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReferredBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DailyReport", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AvgOrderValue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CardRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CashRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreditRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GeneratedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("NetIncome") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TopProducts") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("TotalExpenses") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TotalOrders") + .HasColumnType("integer"); + + b.Property("TotalRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TotalVoids") + .HasColumnType("integer"); + + b.Property("VoidAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "BranchId", "Date") + .IsUnique(); + + b.ToTable("DailyReports"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DeliveryCommissionRate", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Platform") + .HasColumnType("integer"); + + b.Property("RatePercent") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Platform") + .IsUnique(); + + b.ToTable("DeliveryCommissionRates"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("PinCode") + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "Phone") + .IsUnique() + .HasFilter("\"DeletedAt\" IS NULL"); + + b.ToTable("Employees"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Deductions") + .HasColumnType("numeric"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsPaid") + .HasColumnType("boolean"); + + b.Property("MonthYear") + .IsRequired() + .HasColumnType("text"); + + b.Property("NetSalary") + .HasColumnType("numeric"); + + b.Property("OvertimePay") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "MonthYear") + .IsUnique(); + + b.ToTable("EmployeeSalaries"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSchedule", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DayOfWeek") + .HasColumnType("integer"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShiftType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "DayOfWeek") + .IsUnique(); + + b.ToTable("EmployeeSchedules", (string)null); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Expense", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Category") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptImageUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ShiftId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("ShiftId"); + + b.HasIndex("CafeId", "BranchId", "CreatedAt"); + + b.ToTable("Expenses"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LowStockWarningPercent") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ParLevel") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("QuantityOnHand") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("ReorderLevel") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("Unit") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("UnitCost") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Ingredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndDate") + .HasColumnType("date"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("ReviewedBy") + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId"); + + b.ToTable("LeaveRequests"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("Icon") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("IconPresetId") + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("IconStyle") + .HasMaxLength(16) + .HasColumnType("character varying(16)"); + + b.Property("ImageUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("TaxId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("TaxId"); + + b.ToTable("MenuCategories"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CategoryId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Model3dUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("CategoryId"); + + b.ToTable("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItemIngredient", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IngredientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("QuantityPerUnit") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.HasKey("Id"); + + b.HasIndex("IngredientId"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("CafeId", "MenuItemId", "IngredientId") + .IsUnique(); + + b.ToTable("MenuItemIngredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CouponId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeliveryMetaJson") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("DeliveryPlatform") + .HasColumnType("integer"); + + b.Property("DiscountAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("EmployeeId") + .HasColumnType("text"); + + b.Property("ExternalOrderId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("GuestName") + .HasColumnType("text"); + + b.Property("GuestPhone") + .HasColumnType("text"); + + b.Property("GuestTrackingToken") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("OrderType") + .HasColumnType("integer"); + + b.Property("PlatformCommission") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ReservationId") + .HasColumnType("text"); + + b.Property("SnappfoodOrderId") + .HasColumnType("text"); + + b.Property("Source") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("StatusUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Subtotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("TaxTotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Total") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CouponId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("GuestTrackingToken"); + + b.HasIndex("ReservationId"); + + b.HasIndex("TableId"); + + b.HasIndex("CafeId", "DeliveryPlatform", "ExternalOrderId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsVoided") + .HasColumnType("boolean"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UnitPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VoidedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VoidedByUserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Reference") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.PlatformFeature", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DisplayNameEn") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DisplayNameFa") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("IsEnabledGlobally") + .HasColumnType("boolean"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("ModuleGroup") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("PlatformFeatures"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.PlatformPlanDefinition", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DisplayNameEn") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DisplayNameFa") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("FeaturesJson") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsBillableOnline") + .HasColumnType("boolean"); + + b.Property("LimitsJson") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("MonthlyPriceToman") + .HasPrecision(18) + .HasColumnType("numeric(18,0)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("Tier") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Tier") + .IsUnique(); + + b.ToTable("PlatformPlanDefinitions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.PlatformSetting", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DescriptionFa") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(8000) + .HasColumnType("character varying(8000)"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("PlatformSettings"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.QueueTicket", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerLabel") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedByUserId") + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("integer"); + + b.Property("OrderId") + .HasColumnType("text"); + + b.Property("ServiceDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("OrderId"); + + b.HasIndex("CafeId", "BranchId", "ServiceDate", "Number") + .IsUnique(); + + b.ToTable("QueueTickets"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClosedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ClosedByUserId") + .HasColumnType("text"); + + b.Property("ClosingCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Discrepancy") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ExpectedCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("OpenedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OpenedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("OpeningCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("ClosedByUserId"); + + b.HasIndex("OpenedByUserId"); + + b.HasIndex("BranchId", "Status"); + + b.ToTable("RegisterShifts", (string)null); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Delta") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("IngredientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Kind") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("OrderId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("IngredientId"); + + b.HasIndex("CafeId", "OrderId"); + + b.ToTable("StockMovements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AmountRials") + .HasColumnType("bigint"); + + b.Property("AmountToman") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Authority") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Months") + .HasColumnType("integer"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("Provider") + .HasColumnType("integer"); + + b.Property("RefId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Authority"); + + b.HasIndex("CafeId"); + + b.ToTable("SubscriptionPayments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicket", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AssignedAdminId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClosedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByEmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Priority") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subject") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedByEmployeeId"); + + b.HasIndex("CafeId", "Status", "UpdatedAt"); + + b.ToTable("SupportTickets"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicketMessage", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Body") + .IsRequired() + .HasMaxLength(8000) + .HasColumnType("character varying(8000)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SenderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("SenderKind") + .HasColumnType("integer"); + + b.Property("TicketId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("TicketId", "CreatedAt"); + + b.ToTable("SupportTicketMessages"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SystemAdmin", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.HasKey("Id"); + + b.HasIndex("Phone") + .IsUnique(); + + b.ToTable("SystemAdmins"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Floor") + .HasColumnType("text"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsCleaning") + .HasColumnType("boolean"); + + b.Property("Number") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("QrCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("SectionId") + .HasColumnType("text"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("QrCode") + .IsUnique(); + + b.HasIndex("SectionId"); + + b.HasIndex("BranchId", "SectionId", "SortOrder"); + + b.ToTable("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GuestName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GuestPhone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("PartySize") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("time without time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("TableId"); + + b.HasIndex("CafeId", "Date", "Time"); + + b.ToTable("TableReservations"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("BranchId", "Name"); + + b.ToTable("TableSections"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsCompound") + .HasColumnType("boolean"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("IsRequired") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.WebhookLog", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AttemptCount") + .HasColumnType("integer"); + + b.Property("CafeId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("ExternalOrderId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("MeeziOrderId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Platform") + .HasColumnType("integer"); + + b.Property("Processed") + .HasColumnType("boolean"); + + b.Property("ProcessedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RawBody") + .IsRequired() + .HasColumnType("text"); + + b.Property("SignatureHeader") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("SignatureValid") + .HasColumnType("boolean"); + + b.Property("Success") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("Platform", "CreatedAt"); + + b.ToTable("WebhookLogs"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Attendances") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Branches") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.BranchMenuItemOverride", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("BranchOverrides") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("MenuItem"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeFeatureOverride", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", null) + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Reviews") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CashTransaction", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Shift", "Shift") + .WithMany("Transactions") + .HasForeignKey("ShiftId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Shift"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Coupons") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Customers") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DailyReport", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DeliveryCommissionRate", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Staff") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Employees") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Salaries") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSchedule", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Schedules") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Expense", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Shift", "Shift") + .WithMany() + .HasForeignKey("ShiftId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Shift"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Ingredients") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("LeaveRequests") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuCategories") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Tax", "Tax") + .WithMany("MenuCategories") + .HasForeignKey("TaxId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Tax"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuItems") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuCategory", "Category") + .WithMany("MenuItems") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItemIngredient", b => + { + b.HasOne("Meezi.Core.Entities.Ingredient", "Ingredient") + .WithMany("MenuItemRecipes") + .HasForeignKey("IngredientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("RecipeIngredients") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ingredient"); + + b.Navigation("MenuItem"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Orders") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Orders") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Coupon", "Coupon") + .WithMany("Orders") + .HasForeignKey("CouponId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Orders") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.TableReservation", "Reservation") + .WithMany() + .HasForeignKey("ReservationId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany("Orders") + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Coupon"); + + b.Navigation("Customer"); + + b.Navigation("Employee"); + + b.Navigation("Reservation"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("OrderItems") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Items") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MenuItem"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Payments") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.QueueTicket", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Employee", "ClosedBy") + .WithMany() + .HasForeignKey("ClosedByUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "OpenedBy") + .WithMany() + .HasForeignKey("OpenedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("ClosedBy"); + + b.Navigation("OpenedBy"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Ingredient", "Ingredient") + .WithMany("Movements") + .HasForeignKey("IngredientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Ingredient"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("SubscriptionPayments") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicket", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Employee", "CreatedByEmployee") + .WithMany() + .HasForeignKey("CreatedByEmployeeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("CreatedByEmployee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicketMessage", b => + { + b.HasOne("Meezi.Core.Entities.SupportTicket", "Ticket") + .WithMany("Messages") + .HasForeignKey("TicketId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ticket"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Tables") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Tables") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.TableSection", "Section") + .WithMany("Tables") + .HasForeignKey("SectionId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Section"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany() + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Customer"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Sections") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Taxes") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.WebhookLog", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Navigation("Orders"); + + b.Navigation("Sections"); + + b.Navigation("Staff"); + + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Navigation("Branches"); + + b.Navigation("Coupons"); + + b.Navigation("Customers"); + + b.Navigation("Employees"); + + b.Navigation("Ingredients"); + + b.Navigation("MenuCategories"); + + b.Navigation("MenuItems"); + + b.Navigation("Orders"); + + b.Navigation("Reviews"); + + b.Navigation("SubscriptionPayments"); + + b.Navigation("Tables"); + + b.Navigation("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Navigation("Attendances"); + + b.Navigation("LeaveRequests"); + + b.Navigation("Orders"); + + b.Navigation("Salaries"); + + b.Navigation("Schedules"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Navigation("MenuItemRecipes"); + + b.Navigation("Movements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Navigation("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Navigation("BranchOverrides"); + + b.Navigation("OrderItems"); + + b.Navigation("RecipeIngredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Navigation("Items"); + + b.Navigation("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicket", b => + { + b.Navigation("Messages"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Navigation("MenuCategories"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260522195153_AddCafeDiscoverProfileJson.cs b/src/Meezi.Infrastructure/Data/Migrations/20260522195153_AddCafeDiscoverProfileJson.cs new file mode 100644 index 0000000..d1ffc80 --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260522195153_AddCafeDiscoverProfileJson.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + /// + public partial class AddCafeDiscoverProfileJson : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "DiscoverProfileJson", + table: "Cafes", + type: "character varying(8000)", + maxLength: 8000, + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "DiscoverProfileJson", + table: "Cafes"); + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260522211634_OrderDisplayNumber.Designer.cs b/src/Meezi.Infrastructure/Data/Migrations/20260522211634_OrderDisplayNumber.Designer.cs new file mode 100644 index 0000000..493b0ba --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260522211634_OrderDisplayNumber.Designer.cs @@ -0,0 +1,2701 @@ +// +using System; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260522211634_OrderDisplayNumber")] + partial class OrderDisplayNumber + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ClockIn") + .HasColumnType("timestamp with time zone"); + + b.Property("ClockOut") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "Date") + .IsUnique(); + + b.ToTable("Attendances"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccentColor") + .HasColumnType("text"); + + b.Property("Address") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("AutoCutEnabled") + .HasColumnType("boolean"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("City") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("KitchenPrinterIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("KitchenPrinterPort") + .HasColumnType("integer"); + + b.Property("LogoUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("PaperWidthMm") + .HasColumnType("integer"); + + b.Property("Phone") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("PosDeviceIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("PosDevicePort") + .HasColumnType("integer"); + + b.Property("ReceiptFooter") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptHeader") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptPrinterIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("ReceiptPrinterPort") + .HasColumnType("integer"); + + b.Property("TaxRate") + .HasColumnType("numeric"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WelcomeText") + .HasColumnType("text"); + + b.Property("WifiPassword") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "IsActive"); + + b.ToTable("Branches"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.BranchMenuItemOverride", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("PriceOverride") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("SortOrderOverride") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedByUserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("BranchId", "MenuItemId") + .IsUnique(); + + b.ToTable("BranchMenuItemOverrides"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("AllowBranchTaxOverride") + .HasColumnType("boolean"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("CoverImageUrl") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DefaultTaxRate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DigikalaVendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("DiscoverProfileJson") + .HasMaxLength(8000) + .HasColumnType("character varying(8000)"); + + b.Property("IsSuspended") + .HasColumnType("boolean"); + + b.Property("IsVerified") + .HasColumnType("boolean"); + + b.Property("LogoUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("PlanExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("PreferredLanguage") + .IsRequired() + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SnappfoodVendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Tap30VendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ThemeJson") + .HasMaxLength(8000) + .HasColumnType("character varying(8000)"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Cafes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeFeatureOverride", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FeatureKey") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "FeatureKey") + .IsUnique(); + + b.ToTable("CafeFeatureOverrides"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeNotification", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Body") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsRead") + .HasColumnType("boolean"); + + b.Property("ReadAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReferenceId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("TableNumber") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "IsRead", "CreatedAt"); + + b.ToTable("CafeNotifications"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AuthorName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("AuthorPhone") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Comment") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerRepliedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerReply") + .HasColumnType("text"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "CreatedAt"); + + b.ToTable("CafeReviews"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CashTransaction", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("ReferenceId") + .HasColumnType("text"); + + b.Property("ShiftId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("ShiftId"); + + b.HasIndex("CafeId", "BranchId"); + + b.ToTable("CashTransactions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MaxDiscount") + .HasColumnType("numeric"); + + b.Property("MinOrderAmount") + .HasColumnType("numeric"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TargetGroup") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UsageLimit") + .HasColumnType("integer"); + + b.Property("UsedCount") + .HasColumnType("integer"); + + b.Property("Value") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Code") + .IsUnique(); + + b.ToTable("Coupons"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BirthDateJalali") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Group") + .HasColumnType("integer"); + + b.Property("LoyaltyPoints") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReferredBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DailyReport", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AvgOrderValue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CardRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CashRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreditRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GeneratedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("NetIncome") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TopProducts") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("TotalExpenses") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TotalOrders") + .HasColumnType("integer"); + + b.Property("TotalRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TotalVoids") + .HasColumnType("integer"); + + b.Property("VoidAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "BranchId", "Date") + .IsUnique(); + + b.ToTable("DailyReports"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DeliveryCommissionRate", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Platform") + .HasColumnType("integer"); + + b.Property("RatePercent") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Platform") + .IsUnique(); + + b.ToTable("DeliveryCommissionRates"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("PinCode") + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "Phone") + .IsUnique() + .HasFilter("\"DeletedAt\" IS NULL"); + + b.ToTable("Employees"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Deductions") + .HasColumnType("numeric"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsPaid") + .HasColumnType("boolean"); + + b.Property("MonthYear") + .IsRequired() + .HasColumnType("text"); + + b.Property("NetSalary") + .HasColumnType("numeric"); + + b.Property("OvertimePay") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "MonthYear") + .IsUnique(); + + b.ToTable("EmployeeSalaries"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSchedule", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DayOfWeek") + .HasColumnType("integer"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShiftType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "DayOfWeek") + .IsUnique(); + + b.ToTable("EmployeeSchedules", (string)null); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Expense", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Category") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptImageUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ShiftId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("ShiftId"); + + b.HasIndex("CafeId", "BranchId", "CreatedAt"); + + b.ToTable("Expenses"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LowStockWarningPercent") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ParLevel") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("QuantityOnHand") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("ReorderLevel") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("Unit") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("UnitCost") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Ingredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndDate") + .HasColumnType("date"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("ReviewedBy") + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId"); + + b.ToTable("LeaveRequests"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("Icon") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("IconPresetId") + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("IconStyle") + .HasMaxLength(16) + .HasColumnType("character varying(16)"); + + b.Property("ImageUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("TaxId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("TaxId"); + + b.ToTable("MenuCategories"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CategoryId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Model3dUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("CategoryId"); + + b.ToTable("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItemIngredient", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IngredientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("QuantityPerUnit") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.HasKey("Id"); + + b.HasIndex("IngredientId"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("CafeId", "MenuItemId", "IngredientId") + .IsUnique(); + + b.ToTable("MenuItemIngredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CouponId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeliveryMetaJson") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("DeliveryPlatform") + .HasColumnType("integer"); + + b.Property("DiscountAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("DisplayNumber") + .HasColumnType("integer"); + + b.Property("EmployeeId") + .HasColumnType("text"); + + b.Property("ExternalOrderId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("GuestName") + .HasColumnType("text"); + + b.Property("GuestPhone") + .HasColumnType("text"); + + b.Property("GuestTrackingToken") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("OrderType") + .HasColumnType("integer"); + + b.Property("PlatformCommission") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ReservationId") + .HasColumnType("text"); + + b.Property("SnappfoodOrderId") + .HasColumnType("text"); + + b.Property("Source") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("StatusUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Subtotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("TaxTotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Total") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CouponId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("GuestTrackingToken"); + + b.HasIndex("ReservationId"); + + b.HasIndex("TableId"); + + b.HasIndex("CafeId", "DisplayNumber") + .IsUnique(); + + b.HasIndex("CafeId", "DeliveryPlatform", "ExternalOrderId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsVoided") + .HasColumnType("boolean"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UnitPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VoidedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VoidedByUserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Reference") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.PlatformFeature", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DisplayNameEn") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DisplayNameFa") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("IsEnabledGlobally") + .HasColumnType("boolean"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("ModuleGroup") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("PlatformFeatures"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.PlatformPlanDefinition", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DisplayNameEn") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DisplayNameFa") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("FeaturesJson") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsBillableOnline") + .HasColumnType("boolean"); + + b.Property("LimitsJson") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("MonthlyPriceToman") + .HasPrecision(18) + .HasColumnType("numeric(18,0)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("Tier") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Tier") + .IsUnique(); + + b.ToTable("PlatformPlanDefinitions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.PlatformSetting", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DescriptionFa") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(8000) + .HasColumnType("character varying(8000)"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("PlatformSettings"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.QueueTicket", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerLabel") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedByUserId") + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("integer"); + + b.Property("OrderId") + .HasColumnType("text"); + + b.Property("ServiceDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("OrderId"); + + b.HasIndex("CafeId", "BranchId", "ServiceDate", "Number") + .IsUnique(); + + b.ToTable("QueueTickets"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClosedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ClosedByUserId") + .HasColumnType("text"); + + b.Property("ClosingCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Discrepancy") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ExpectedCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("OpenedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OpenedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("OpeningCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("ClosedByUserId"); + + b.HasIndex("OpenedByUserId"); + + b.HasIndex("BranchId", "Status"); + + b.ToTable("RegisterShifts", (string)null); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Delta") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("IngredientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Kind") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("OrderId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("IngredientId"); + + b.HasIndex("CafeId", "OrderId"); + + b.ToTable("StockMovements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AmountRials") + .HasColumnType("bigint"); + + b.Property("AmountToman") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Authority") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Months") + .HasColumnType("integer"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("Provider") + .HasColumnType("integer"); + + b.Property("RefId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Authority"); + + b.HasIndex("CafeId"); + + b.ToTable("SubscriptionPayments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicket", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AssignedAdminId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClosedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByEmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Priority") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subject") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedByEmployeeId"); + + b.HasIndex("CafeId", "Status", "UpdatedAt"); + + b.ToTable("SupportTickets"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicketMessage", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Body") + .IsRequired() + .HasMaxLength(8000) + .HasColumnType("character varying(8000)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SenderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("SenderKind") + .HasColumnType("integer"); + + b.Property("TicketId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("TicketId", "CreatedAt"); + + b.ToTable("SupportTicketMessages"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SystemAdmin", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.HasKey("Id"); + + b.HasIndex("Phone") + .IsUnique(); + + b.ToTable("SystemAdmins"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Floor") + .HasColumnType("text"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsCleaning") + .HasColumnType("boolean"); + + b.Property("Number") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("QrCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("SectionId") + .HasColumnType("text"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("QrCode") + .IsUnique(); + + b.HasIndex("SectionId"); + + b.HasIndex("BranchId", "SectionId", "SortOrder"); + + b.ToTable("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GuestName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GuestPhone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("PartySize") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("time without time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("TableId"); + + b.HasIndex("CafeId", "Date", "Time"); + + b.ToTable("TableReservations"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("BranchId", "Name"); + + b.ToTable("TableSections"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsCompound") + .HasColumnType("boolean"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("IsRequired") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.WebhookLog", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AttemptCount") + .HasColumnType("integer"); + + b.Property("CafeId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("ExternalOrderId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("MeeziOrderId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Platform") + .HasColumnType("integer"); + + b.Property("Processed") + .HasColumnType("boolean"); + + b.Property("ProcessedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RawBody") + .IsRequired() + .HasColumnType("text"); + + b.Property("SignatureHeader") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("SignatureValid") + .HasColumnType("boolean"); + + b.Property("Success") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("Platform", "CreatedAt"); + + b.ToTable("WebhookLogs"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Attendances") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Branches") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.BranchMenuItemOverride", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("BranchOverrides") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("MenuItem"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeFeatureOverride", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", null) + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Reviews") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CashTransaction", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Shift", "Shift") + .WithMany("Transactions") + .HasForeignKey("ShiftId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Shift"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Coupons") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Customers") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DailyReport", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DeliveryCommissionRate", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Staff") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Employees") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Salaries") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSchedule", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Schedules") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Expense", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Shift", "Shift") + .WithMany() + .HasForeignKey("ShiftId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Shift"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Ingredients") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("LeaveRequests") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuCategories") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Tax", "Tax") + .WithMany("MenuCategories") + .HasForeignKey("TaxId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Tax"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuItems") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuCategory", "Category") + .WithMany("MenuItems") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItemIngredient", b => + { + b.HasOne("Meezi.Core.Entities.Ingredient", "Ingredient") + .WithMany("MenuItemRecipes") + .HasForeignKey("IngredientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("RecipeIngredients") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ingredient"); + + b.Navigation("MenuItem"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Orders") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Orders") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Coupon", "Coupon") + .WithMany("Orders") + .HasForeignKey("CouponId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Orders") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.TableReservation", "Reservation") + .WithMany() + .HasForeignKey("ReservationId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany("Orders") + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Coupon"); + + b.Navigation("Customer"); + + b.Navigation("Employee"); + + b.Navigation("Reservation"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("OrderItems") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Items") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MenuItem"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Payments") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.QueueTicket", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Employee", "ClosedBy") + .WithMany() + .HasForeignKey("ClosedByUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "OpenedBy") + .WithMany() + .HasForeignKey("OpenedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("ClosedBy"); + + b.Navigation("OpenedBy"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Ingredient", "Ingredient") + .WithMany("Movements") + .HasForeignKey("IngredientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Ingredient"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("SubscriptionPayments") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicket", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Employee", "CreatedByEmployee") + .WithMany() + .HasForeignKey("CreatedByEmployeeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("CreatedByEmployee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicketMessage", b => + { + b.HasOne("Meezi.Core.Entities.SupportTicket", "Ticket") + .WithMany("Messages") + .HasForeignKey("TicketId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ticket"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Tables") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Tables") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.TableSection", "Section") + .WithMany("Tables") + .HasForeignKey("SectionId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Section"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany() + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Customer"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Sections") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Taxes") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.WebhookLog", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Navigation("Orders"); + + b.Navigation("Sections"); + + b.Navigation("Staff"); + + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Navigation("Branches"); + + b.Navigation("Coupons"); + + b.Navigation("Customers"); + + b.Navigation("Employees"); + + b.Navigation("Ingredients"); + + b.Navigation("MenuCategories"); + + b.Navigation("MenuItems"); + + b.Navigation("Orders"); + + b.Navigation("Reviews"); + + b.Navigation("SubscriptionPayments"); + + b.Navigation("Tables"); + + b.Navigation("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Navigation("Attendances"); + + b.Navigation("LeaveRequests"); + + b.Navigation("Orders"); + + b.Navigation("Salaries"); + + b.Navigation("Schedules"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Navigation("MenuItemRecipes"); + + b.Navigation("Movements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Navigation("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Navigation("BranchOverrides"); + + b.Navigation("OrderItems"); + + b.Navigation("RecipeIngredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Navigation("Items"); + + b.Navigation("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicket", b => + { + b.Navigation("Messages"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Navigation("MenuCategories"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260522211634_OrderDisplayNumber.cs b/src/Meezi.Infrastructure/Data/Migrations/20260522211634_OrderDisplayNumber.cs new file mode 100644 index 0000000..2a8e3a6 --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260522211634_OrderDisplayNumber.cs @@ -0,0 +1,52 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + /// + public partial class OrderDisplayNumber : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "DisplayNumber", + table: "Orders", + type: "integer", + nullable: false, + defaultValue: 0); + + migrationBuilder.Sql( + """ + WITH numbered AS ( + SELECT "Id", + ROW_NUMBER() OVER (PARTITION BY "CafeId" ORDER BY "CreatedAt", "Id") AS rn + FROM "Orders" + ) + UPDATE "Orders" o + SET "DisplayNumber" = n.rn + FROM numbered n + WHERE o."Id" = n."Id"; + """); + + migrationBuilder.CreateIndex( + name: "IX_Orders_CafeId_DisplayNumber", + table: "Orders", + columns: new[] { "CafeId", "DisplayNumber" }, + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_Orders_CafeId_DisplayNumber", + table: "Orders"); + + migrationBuilder.DropColumn( + name: "DisplayNumber", + table: "Orders"); + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260522212251_StockMovementPurchaseCost.Designer.cs b/src/Meezi.Infrastructure/Data/Migrations/20260522212251_StockMovementPurchaseCost.Designer.cs new file mode 100644 index 0000000..d856516 --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260522212251_StockMovementPurchaseCost.Designer.cs @@ -0,0 +1,2713 @@ +// +using System; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260522212251_StockMovementPurchaseCost")] + partial class StockMovementPurchaseCost + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ClockIn") + .HasColumnType("timestamp with time zone"); + + b.Property("ClockOut") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "Date") + .IsUnique(); + + b.ToTable("Attendances"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccentColor") + .HasColumnType("text"); + + b.Property("Address") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("AutoCutEnabled") + .HasColumnType("boolean"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("City") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("KitchenPrinterIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("KitchenPrinterPort") + .HasColumnType("integer"); + + b.Property("LogoUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("PaperWidthMm") + .HasColumnType("integer"); + + b.Property("Phone") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("PosDeviceIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("PosDevicePort") + .HasColumnType("integer"); + + b.Property("ReceiptFooter") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptHeader") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptPrinterIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("ReceiptPrinterPort") + .HasColumnType("integer"); + + b.Property("TaxRate") + .HasColumnType("numeric"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WelcomeText") + .HasColumnType("text"); + + b.Property("WifiPassword") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "IsActive"); + + b.ToTable("Branches"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.BranchMenuItemOverride", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("PriceOverride") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("SortOrderOverride") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedByUserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("BranchId", "MenuItemId") + .IsUnique(); + + b.ToTable("BranchMenuItemOverrides"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("AllowBranchTaxOverride") + .HasColumnType("boolean"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("CoverImageUrl") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DefaultTaxRate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DigikalaVendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("DiscoverProfileJson") + .HasMaxLength(8000) + .HasColumnType("character varying(8000)"); + + b.Property("IsSuspended") + .HasColumnType("boolean"); + + b.Property("IsVerified") + .HasColumnType("boolean"); + + b.Property("LogoUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("PlanExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("PreferredLanguage") + .IsRequired() + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SnappfoodVendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Tap30VendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ThemeJson") + .HasMaxLength(8000) + .HasColumnType("character varying(8000)"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Cafes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeFeatureOverride", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FeatureKey") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "FeatureKey") + .IsUnique(); + + b.ToTable("CafeFeatureOverrides"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeNotification", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Body") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsRead") + .HasColumnType("boolean"); + + b.Property("ReadAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReferenceId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("TableNumber") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "IsRead", "CreatedAt"); + + b.ToTable("CafeNotifications"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AuthorName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("AuthorPhone") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Comment") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerRepliedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerReply") + .HasColumnType("text"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "CreatedAt"); + + b.ToTable("CafeReviews"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CashTransaction", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("ReferenceId") + .HasColumnType("text"); + + b.Property("ShiftId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("ShiftId"); + + b.HasIndex("CafeId", "BranchId"); + + b.ToTable("CashTransactions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MaxDiscount") + .HasColumnType("numeric"); + + b.Property("MinOrderAmount") + .HasColumnType("numeric"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TargetGroup") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UsageLimit") + .HasColumnType("integer"); + + b.Property("UsedCount") + .HasColumnType("integer"); + + b.Property("Value") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Code") + .IsUnique(); + + b.ToTable("Coupons"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BirthDateJalali") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Group") + .HasColumnType("integer"); + + b.Property("LoyaltyPoints") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReferredBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DailyReport", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AvgOrderValue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CardRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CashRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreditRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GeneratedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("NetIncome") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TopProducts") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("TotalExpenses") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TotalOrders") + .HasColumnType("integer"); + + b.Property("TotalRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TotalVoids") + .HasColumnType("integer"); + + b.Property("VoidAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "BranchId", "Date") + .IsUnique(); + + b.ToTable("DailyReports"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DeliveryCommissionRate", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Platform") + .HasColumnType("integer"); + + b.Property("RatePercent") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Platform") + .IsUnique(); + + b.ToTable("DeliveryCommissionRates"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("PinCode") + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "Phone") + .IsUnique() + .HasFilter("\"DeletedAt\" IS NULL"); + + b.ToTable("Employees"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Deductions") + .HasColumnType("numeric"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsPaid") + .HasColumnType("boolean"); + + b.Property("MonthYear") + .IsRequired() + .HasColumnType("text"); + + b.Property("NetSalary") + .HasColumnType("numeric"); + + b.Property("OvertimePay") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "MonthYear") + .IsUnique(); + + b.ToTable("EmployeeSalaries"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSchedule", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DayOfWeek") + .HasColumnType("integer"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShiftType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "DayOfWeek") + .IsUnique(); + + b.ToTable("EmployeeSchedules", (string)null); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Expense", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Category") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptImageUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ShiftId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("ShiftId"); + + b.HasIndex("CafeId", "BranchId", "CreatedAt"); + + b.ToTable("Expenses"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LowStockWarningPercent") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ParLevel") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("QuantityOnHand") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("ReorderLevel") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("Unit") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("UnitCost") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Ingredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndDate") + .HasColumnType("date"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("ReviewedBy") + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId"); + + b.ToTable("LeaveRequests"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("Icon") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("IconPresetId") + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("IconStyle") + .HasMaxLength(16) + .HasColumnType("character varying(16)"); + + b.Property("ImageUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("TaxId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("TaxId"); + + b.ToTable("MenuCategories"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CategoryId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Model3dUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("CategoryId"); + + b.ToTable("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItemIngredient", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IngredientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("QuantityPerUnit") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.HasKey("Id"); + + b.HasIndex("IngredientId"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("CafeId", "MenuItemId", "IngredientId") + .IsUnique(); + + b.ToTable("MenuItemIngredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CouponId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeliveryMetaJson") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("DeliveryPlatform") + .HasColumnType("integer"); + + b.Property("DiscountAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("DisplayNumber") + .HasColumnType("integer"); + + b.Property("EmployeeId") + .HasColumnType("text"); + + b.Property("ExternalOrderId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("GuestName") + .HasColumnType("text"); + + b.Property("GuestPhone") + .HasColumnType("text"); + + b.Property("GuestTrackingToken") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("OrderType") + .HasColumnType("integer"); + + b.Property("PlatformCommission") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ReservationId") + .HasColumnType("text"); + + b.Property("SnappfoodOrderId") + .HasColumnType("text"); + + b.Property("Source") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("StatusUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Subtotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("TaxTotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Total") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CouponId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("GuestTrackingToken"); + + b.HasIndex("ReservationId"); + + b.HasIndex("TableId"); + + b.HasIndex("CafeId", "DisplayNumber") + .IsUnique(); + + b.HasIndex("CafeId", "DeliveryPlatform", "ExternalOrderId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsVoided") + .HasColumnType("boolean"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UnitPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VoidedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VoidedByUserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Reference") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.PlatformFeature", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DisplayNameEn") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DisplayNameFa") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("IsEnabledGlobally") + .HasColumnType("boolean"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("ModuleGroup") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("PlatformFeatures"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.PlatformPlanDefinition", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DisplayNameEn") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DisplayNameFa") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("FeaturesJson") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsBillableOnline") + .HasColumnType("boolean"); + + b.Property("LimitsJson") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("MonthlyPriceToman") + .HasPrecision(18) + .HasColumnType("numeric(18,0)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("Tier") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Tier") + .IsUnique(); + + b.ToTable("PlatformPlanDefinitions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.PlatformSetting", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DescriptionFa") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(8000) + .HasColumnType("character varying(8000)"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("PlatformSettings"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.QueueTicket", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerLabel") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedByUserId") + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("integer"); + + b.Property("OrderId") + .HasColumnType("text"); + + b.Property("ServiceDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("OrderId"); + + b.HasIndex("CafeId", "BranchId", "ServiceDate", "Number") + .IsUnique(); + + b.ToTable("QueueTickets"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClosedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ClosedByUserId") + .HasColumnType("text"); + + b.Property("ClosingCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Discrepancy") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ExpectedCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("OpenedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OpenedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("OpeningCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("ClosedByUserId"); + + b.HasIndex("OpenedByUserId"); + + b.HasIndex("BranchId", "Status"); + + b.ToTable("RegisterShifts", (string)null); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Delta") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("ExpenseId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("IngredientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Kind") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("OrderId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("TotalCostToman") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("IngredientId"); + + b.HasIndex("CafeId", "OrderId"); + + b.ToTable("StockMovements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AmountRials") + .HasColumnType("bigint"); + + b.Property("AmountToman") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Authority") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Months") + .HasColumnType("integer"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("Provider") + .HasColumnType("integer"); + + b.Property("RefId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Authority"); + + b.HasIndex("CafeId"); + + b.ToTable("SubscriptionPayments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicket", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AssignedAdminId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClosedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByEmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Priority") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subject") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedByEmployeeId"); + + b.HasIndex("CafeId", "Status", "UpdatedAt"); + + b.ToTable("SupportTickets"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicketMessage", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Body") + .IsRequired() + .HasMaxLength(8000) + .HasColumnType("character varying(8000)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SenderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("SenderKind") + .HasColumnType("integer"); + + b.Property("TicketId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("TicketId", "CreatedAt"); + + b.ToTable("SupportTicketMessages"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SystemAdmin", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.HasKey("Id"); + + b.HasIndex("Phone") + .IsUnique(); + + b.ToTable("SystemAdmins"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Floor") + .HasColumnType("text"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsCleaning") + .HasColumnType("boolean"); + + b.Property("Number") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("QrCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("SectionId") + .HasColumnType("text"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("QrCode") + .IsUnique(); + + b.HasIndex("SectionId"); + + b.HasIndex("BranchId", "SectionId", "SortOrder"); + + b.ToTable("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GuestName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GuestPhone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("PartySize") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("time without time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("TableId"); + + b.HasIndex("CafeId", "Date", "Time"); + + b.ToTable("TableReservations"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("BranchId", "Name"); + + b.ToTable("TableSections"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsCompound") + .HasColumnType("boolean"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("IsRequired") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.WebhookLog", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AttemptCount") + .HasColumnType("integer"); + + b.Property("CafeId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("ExternalOrderId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("MeeziOrderId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Platform") + .HasColumnType("integer"); + + b.Property("Processed") + .HasColumnType("boolean"); + + b.Property("ProcessedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RawBody") + .IsRequired() + .HasColumnType("text"); + + b.Property("SignatureHeader") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("SignatureValid") + .HasColumnType("boolean"); + + b.Property("Success") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("Platform", "CreatedAt"); + + b.ToTable("WebhookLogs"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Attendances") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Branches") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.BranchMenuItemOverride", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("BranchOverrides") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("MenuItem"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeFeatureOverride", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", null) + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Reviews") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CashTransaction", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Shift", "Shift") + .WithMany("Transactions") + .HasForeignKey("ShiftId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Shift"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Coupons") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Customers") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DailyReport", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DeliveryCommissionRate", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Staff") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Employees") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Salaries") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSchedule", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Schedules") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Expense", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Shift", "Shift") + .WithMany() + .HasForeignKey("ShiftId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Shift"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Ingredients") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("LeaveRequests") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuCategories") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Tax", "Tax") + .WithMany("MenuCategories") + .HasForeignKey("TaxId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Tax"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuItems") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuCategory", "Category") + .WithMany("MenuItems") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItemIngredient", b => + { + b.HasOne("Meezi.Core.Entities.Ingredient", "Ingredient") + .WithMany("MenuItemRecipes") + .HasForeignKey("IngredientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("RecipeIngredients") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ingredient"); + + b.Navigation("MenuItem"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Orders") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Orders") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Coupon", "Coupon") + .WithMany("Orders") + .HasForeignKey("CouponId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Orders") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.TableReservation", "Reservation") + .WithMany() + .HasForeignKey("ReservationId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany("Orders") + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Coupon"); + + b.Navigation("Customer"); + + b.Navigation("Employee"); + + b.Navigation("Reservation"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("OrderItems") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Items") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MenuItem"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Payments") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.QueueTicket", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Employee", "ClosedBy") + .WithMany() + .HasForeignKey("ClosedByUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "OpenedBy") + .WithMany() + .HasForeignKey("OpenedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("ClosedBy"); + + b.Navigation("OpenedBy"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Ingredient", "Ingredient") + .WithMany("Movements") + .HasForeignKey("IngredientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Ingredient"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("SubscriptionPayments") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicket", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Employee", "CreatedByEmployee") + .WithMany() + .HasForeignKey("CreatedByEmployeeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("CreatedByEmployee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicketMessage", b => + { + b.HasOne("Meezi.Core.Entities.SupportTicket", "Ticket") + .WithMany("Messages") + .HasForeignKey("TicketId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ticket"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Tables") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Tables") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.TableSection", "Section") + .WithMany("Tables") + .HasForeignKey("SectionId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Section"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany() + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Customer"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Sections") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Taxes") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.WebhookLog", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Navigation("Orders"); + + b.Navigation("Sections"); + + b.Navigation("Staff"); + + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Navigation("Branches"); + + b.Navigation("Coupons"); + + b.Navigation("Customers"); + + b.Navigation("Employees"); + + b.Navigation("Ingredients"); + + b.Navigation("MenuCategories"); + + b.Navigation("MenuItems"); + + b.Navigation("Orders"); + + b.Navigation("Reviews"); + + b.Navigation("SubscriptionPayments"); + + b.Navigation("Tables"); + + b.Navigation("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Navigation("Attendances"); + + b.Navigation("LeaveRequests"); + + b.Navigation("Orders"); + + b.Navigation("Salaries"); + + b.Navigation("Schedules"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Navigation("MenuItemRecipes"); + + b.Navigation("Movements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Navigation("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Navigation("BranchOverrides"); + + b.Navigation("OrderItems"); + + b.Navigation("RecipeIngredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Navigation("Items"); + + b.Navigation("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicket", b => + { + b.Navigation("Messages"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Navigation("MenuCategories"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260522212251_StockMovementPurchaseCost.cs b/src/Meezi.Infrastructure/Data/Migrations/20260522212251_StockMovementPurchaseCost.cs new file mode 100644 index 0000000..59791cd --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260522212251_StockMovementPurchaseCost.cs @@ -0,0 +1,52 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + /// + public partial class StockMovementPurchaseCost : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "BranchId", + table: "StockMovements", + type: "character varying(64)", + maxLength: 64, + nullable: true); + + migrationBuilder.AddColumn( + name: "ExpenseId", + table: "StockMovements", + type: "character varying(64)", + maxLength: 64, + nullable: true); + + migrationBuilder.AddColumn( + name: "TotalCostToman", + table: "StockMovements", + type: "numeric(18,2)", + precision: 18, + scale: 2, + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "BranchId", + table: "StockMovements"); + + migrationBuilder.DropColumn( + name: "ExpenseId", + table: "StockMovements"); + + migrationBuilder.DropColumn( + name: "TotalCostToman", + table: "StockMovements"); + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260522220031_BranchScheduledPermanentDelete.Designer.cs b/src/Meezi.Infrastructure/Data/Migrations/20260522220031_BranchScheduledPermanentDelete.Designer.cs new file mode 100644 index 0000000..20ea07d --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260522220031_BranchScheduledPermanentDelete.Designer.cs @@ -0,0 +1,2716 @@ +// +using System; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260522220031_BranchScheduledPermanentDelete")] + partial class BranchScheduledPermanentDelete + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ClockIn") + .HasColumnType("timestamp with time zone"); + + b.Property("ClockOut") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "Date") + .IsUnique(); + + b.ToTable("Attendances"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccentColor") + .HasColumnType("text"); + + b.Property("Address") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("AutoCutEnabled") + .HasColumnType("boolean"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("City") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("KitchenPrinterIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("KitchenPrinterPort") + .HasColumnType("integer"); + + b.Property("LogoUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("PaperWidthMm") + .HasColumnType("integer"); + + b.Property("Phone") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("PosDeviceIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("PosDevicePort") + .HasColumnType("integer"); + + b.Property("ReceiptFooter") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptHeader") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptPrinterIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("ReceiptPrinterPort") + .HasColumnType("integer"); + + b.Property("ScheduledPermanentDeleteAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TaxRate") + .HasColumnType("numeric"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WelcomeText") + .HasColumnType("text"); + + b.Property("WifiPassword") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "IsActive"); + + b.ToTable("Branches"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.BranchMenuItemOverride", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("PriceOverride") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("SortOrderOverride") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedByUserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("BranchId", "MenuItemId") + .IsUnique(); + + b.ToTable("BranchMenuItemOverrides"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("AllowBranchTaxOverride") + .HasColumnType("boolean"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("CoverImageUrl") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DefaultTaxRate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DigikalaVendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("DiscoverProfileJson") + .HasMaxLength(8000) + .HasColumnType("character varying(8000)"); + + b.Property("IsSuspended") + .HasColumnType("boolean"); + + b.Property("IsVerified") + .HasColumnType("boolean"); + + b.Property("LogoUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("PlanExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("PreferredLanguage") + .IsRequired() + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SnappfoodVendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Tap30VendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ThemeJson") + .HasMaxLength(8000) + .HasColumnType("character varying(8000)"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Cafes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeFeatureOverride", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FeatureKey") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "FeatureKey") + .IsUnique(); + + b.ToTable("CafeFeatureOverrides"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeNotification", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Body") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsRead") + .HasColumnType("boolean"); + + b.Property("ReadAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReferenceId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("TableNumber") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "IsRead", "CreatedAt"); + + b.ToTable("CafeNotifications"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AuthorName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("AuthorPhone") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Comment") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerRepliedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerReply") + .HasColumnType("text"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "CreatedAt"); + + b.ToTable("CafeReviews"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CashTransaction", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("ReferenceId") + .HasColumnType("text"); + + b.Property("ShiftId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("ShiftId"); + + b.HasIndex("CafeId", "BranchId"); + + b.ToTable("CashTransactions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MaxDiscount") + .HasColumnType("numeric"); + + b.Property("MinOrderAmount") + .HasColumnType("numeric"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TargetGroup") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UsageLimit") + .HasColumnType("integer"); + + b.Property("UsedCount") + .HasColumnType("integer"); + + b.Property("Value") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Code") + .IsUnique(); + + b.ToTable("Coupons"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BirthDateJalali") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Group") + .HasColumnType("integer"); + + b.Property("LoyaltyPoints") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReferredBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DailyReport", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AvgOrderValue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CardRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CashRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreditRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GeneratedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("NetIncome") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TopProducts") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("TotalExpenses") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TotalOrders") + .HasColumnType("integer"); + + b.Property("TotalRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TotalVoids") + .HasColumnType("integer"); + + b.Property("VoidAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "BranchId", "Date") + .IsUnique(); + + b.ToTable("DailyReports"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DeliveryCommissionRate", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Platform") + .HasColumnType("integer"); + + b.Property("RatePercent") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Platform") + .IsUnique(); + + b.ToTable("DeliveryCommissionRates"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("PinCode") + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "Phone") + .IsUnique() + .HasFilter("\"DeletedAt\" IS NULL"); + + b.ToTable("Employees"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Deductions") + .HasColumnType("numeric"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsPaid") + .HasColumnType("boolean"); + + b.Property("MonthYear") + .IsRequired() + .HasColumnType("text"); + + b.Property("NetSalary") + .HasColumnType("numeric"); + + b.Property("OvertimePay") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "MonthYear") + .IsUnique(); + + b.ToTable("EmployeeSalaries"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSchedule", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DayOfWeek") + .HasColumnType("integer"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShiftType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "DayOfWeek") + .IsUnique(); + + b.ToTable("EmployeeSchedules", (string)null); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Expense", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Category") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptImageUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ShiftId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("ShiftId"); + + b.HasIndex("CafeId", "BranchId", "CreatedAt"); + + b.ToTable("Expenses"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LowStockWarningPercent") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ParLevel") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("QuantityOnHand") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("ReorderLevel") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("Unit") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("UnitCost") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Ingredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndDate") + .HasColumnType("date"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("ReviewedBy") + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId"); + + b.ToTable("LeaveRequests"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("Icon") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("IconPresetId") + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("IconStyle") + .HasMaxLength(16) + .HasColumnType("character varying(16)"); + + b.Property("ImageUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("TaxId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("TaxId"); + + b.ToTable("MenuCategories"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CategoryId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Model3dUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("CategoryId"); + + b.ToTable("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItemIngredient", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IngredientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("QuantityPerUnit") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.HasKey("Id"); + + b.HasIndex("IngredientId"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("CafeId", "MenuItemId", "IngredientId") + .IsUnique(); + + b.ToTable("MenuItemIngredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CouponId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeliveryMetaJson") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("DeliveryPlatform") + .HasColumnType("integer"); + + b.Property("DiscountAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("DisplayNumber") + .HasColumnType("integer"); + + b.Property("EmployeeId") + .HasColumnType("text"); + + b.Property("ExternalOrderId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("GuestName") + .HasColumnType("text"); + + b.Property("GuestPhone") + .HasColumnType("text"); + + b.Property("GuestTrackingToken") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("OrderType") + .HasColumnType("integer"); + + b.Property("PlatformCommission") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ReservationId") + .HasColumnType("text"); + + b.Property("SnappfoodOrderId") + .HasColumnType("text"); + + b.Property("Source") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("StatusUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Subtotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("TaxTotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Total") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CouponId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("GuestTrackingToken"); + + b.HasIndex("ReservationId"); + + b.HasIndex("TableId"); + + b.HasIndex("CafeId", "DisplayNumber") + .IsUnique(); + + b.HasIndex("CafeId", "DeliveryPlatform", "ExternalOrderId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsVoided") + .HasColumnType("boolean"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UnitPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VoidedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VoidedByUserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Reference") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.PlatformFeature", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DisplayNameEn") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DisplayNameFa") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("IsEnabledGlobally") + .HasColumnType("boolean"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("ModuleGroup") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("PlatformFeatures"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.PlatformPlanDefinition", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DisplayNameEn") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DisplayNameFa") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("FeaturesJson") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsBillableOnline") + .HasColumnType("boolean"); + + b.Property("LimitsJson") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("MonthlyPriceToman") + .HasPrecision(18) + .HasColumnType("numeric(18,0)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("Tier") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Tier") + .IsUnique(); + + b.ToTable("PlatformPlanDefinitions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.PlatformSetting", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DescriptionFa") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(8000) + .HasColumnType("character varying(8000)"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("PlatformSettings"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.QueueTicket", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerLabel") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedByUserId") + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("integer"); + + b.Property("OrderId") + .HasColumnType("text"); + + b.Property("ServiceDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("OrderId"); + + b.HasIndex("CafeId", "BranchId", "ServiceDate", "Number") + .IsUnique(); + + b.ToTable("QueueTickets"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClosedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ClosedByUserId") + .HasColumnType("text"); + + b.Property("ClosingCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Discrepancy") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ExpectedCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("OpenedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OpenedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("OpeningCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("ClosedByUserId"); + + b.HasIndex("OpenedByUserId"); + + b.HasIndex("BranchId", "Status"); + + b.ToTable("RegisterShifts", (string)null); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Delta") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("ExpenseId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("IngredientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Kind") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("OrderId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("TotalCostToman") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("IngredientId"); + + b.HasIndex("CafeId", "OrderId"); + + b.ToTable("StockMovements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AmountRials") + .HasColumnType("bigint"); + + b.Property("AmountToman") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Authority") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Months") + .HasColumnType("integer"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("Provider") + .HasColumnType("integer"); + + b.Property("RefId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Authority"); + + b.HasIndex("CafeId"); + + b.ToTable("SubscriptionPayments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicket", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AssignedAdminId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClosedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByEmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Priority") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subject") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedByEmployeeId"); + + b.HasIndex("CafeId", "Status", "UpdatedAt"); + + b.ToTable("SupportTickets"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicketMessage", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Body") + .IsRequired() + .HasMaxLength(8000) + .HasColumnType("character varying(8000)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SenderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("SenderKind") + .HasColumnType("integer"); + + b.Property("TicketId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("TicketId", "CreatedAt"); + + b.ToTable("SupportTicketMessages"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SystemAdmin", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.HasKey("Id"); + + b.HasIndex("Phone") + .IsUnique(); + + b.ToTable("SystemAdmins"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Floor") + .HasColumnType("text"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsCleaning") + .HasColumnType("boolean"); + + b.Property("Number") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("QrCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("SectionId") + .HasColumnType("text"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("QrCode") + .IsUnique(); + + b.HasIndex("SectionId"); + + b.HasIndex("BranchId", "SectionId", "SortOrder"); + + b.ToTable("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GuestName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GuestPhone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("PartySize") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("time without time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("TableId"); + + b.HasIndex("CafeId", "Date", "Time"); + + b.ToTable("TableReservations"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("BranchId", "Name"); + + b.ToTable("TableSections"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsCompound") + .HasColumnType("boolean"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("IsRequired") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.WebhookLog", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AttemptCount") + .HasColumnType("integer"); + + b.Property("CafeId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("ExternalOrderId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("MeeziOrderId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Platform") + .HasColumnType("integer"); + + b.Property("Processed") + .HasColumnType("boolean"); + + b.Property("ProcessedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RawBody") + .IsRequired() + .HasColumnType("text"); + + b.Property("SignatureHeader") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("SignatureValid") + .HasColumnType("boolean"); + + b.Property("Success") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("Platform", "CreatedAt"); + + b.ToTable("WebhookLogs"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Attendances") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Branches") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.BranchMenuItemOverride", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("BranchOverrides") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("MenuItem"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeFeatureOverride", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", null) + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Reviews") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CashTransaction", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Shift", "Shift") + .WithMany("Transactions") + .HasForeignKey("ShiftId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Shift"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Coupons") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Customers") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DailyReport", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DeliveryCommissionRate", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Staff") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Employees") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Salaries") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSchedule", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Schedules") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Expense", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Shift", "Shift") + .WithMany() + .HasForeignKey("ShiftId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Shift"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Ingredients") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("LeaveRequests") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuCategories") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Tax", "Tax") + .WithMany("MenuCategories") + .HasForeignKey("TaxId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Tax"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuItems") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuCategory", "Category") + .WithMany("MenuItems") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItemIngredient", b => + { + b.HasOne("Meezi.Core.Entities.Ingredient", "Ingredient") + .WithMany("MenuItemRecipes") + .HasForeignKey("IngredientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("RecipeIngredients") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ingredient"); + + b.Navigation("MenuItem"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Orders") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Orders") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Coupon", "Coupon") + .WithMany("Orders") + .HasForeignKey("CouponId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Orders") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.TableReservation", "Reservation") + .WithMany() + .HasForeignKey("ReservationId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany("Orders") + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Coupon"); + + b.Navigation("Customer"); + + b.Navigation("Employee"); + + b.Navigation("Reservation"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("OrderItems") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Items") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MenuItem"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Payments") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.QueueTicket", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Employee", "ClosedBy") + .WithMany() + .HasForeignKey("ClosedByUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "OpenedBy") + .WithMany() + .HasForeignKey("OpenedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("ClosedBy"); + + b.Navigation("OpenedBy"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Ingredient", "Ingredient") + .WithMany("Movements") + .HasForeignKey("IngredientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Ingredient"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("SubscriptionPayments") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicket", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Employee", "CreatedByEmployee") + .WithMany() + .HasForeignKey("CreatedByEmployeeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("CreatedByEmployee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicketMessage", b => + { + b.HasOne("Meezi.Core.Entities.SupportTicket", "Ticket") + .WithMany("Messages") + .HasForeignKey("TicketId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ticket"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Tables") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Tables") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.TableSection", "Section") + .WithMany("Tables") + .HasForeignKey("SectionId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Section"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany() + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Customer"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Sections") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Taxes") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.WebhookLog", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Navigation("Orders"); + + b.Navigation("Sections"); + + b.Navigation("Staff"); + + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Navigation("Branches"); + + b.Navigation("Coupons"); + + b.Navigation("Customers"); + + b.Navigation("Employees"); + + b.Navigation("Ingredients"); + + b.Navigation("MenuCategories"); + + b.Navigation("MenuItems"); + + b.Navigation("Orders"); + + b.Navigation("Reviews"); + + b.Navigation("SubscriptionPayments"); + + b.Navigation("Tables"); + + b.Navigation("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Navigation("Attendances"); + + b.Navigation("LeaveRequests"); + + b.Navigation("Orders"); + + b.Navigation("Salaries"); + + b.Navigation("Schedules"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Navigation("MenuItemRecipes"); + + b.Navigation("Movements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Navigation("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Navigation("BranchOverrides"); + + b.Navigation("OrderItems"); + + b.Navigation("RecipeIngredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Navigation("Items"); + + b.Navigation("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicket", b => + { + b.Navigation("Messages"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Navigation("MenuCategories"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260522220031_BranchScheduledPermanentDelete.cs b/src/Meezi.Infrastructure/Data/Migrations/20260522220031_BranchScheduledPermanentDelete.cs new file mode 100644 index 0000000..7a1ff1b --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260522220031_BranchScheduledPermanentDelete.cs @@ -0,0 +1,29 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + /// + public partial class BranchScheduledPermanentDelete : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ScheduledPermanentDeleteAt", + table: "Branches", + type: "timestamp with time zone", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ScheduledPermanentDeleteAt", + table: "Branches"); + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260522225951_GrowthOpsFeatures.Designer.cs b/src/Meezi.Infrastructure/Data/Migrations/20260522225951_GrowthOpsFeatures.Designer.cs new file mode 100644 index 0000000..01ce575 --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260522225951_GrowthOpsFeatures.Designer.cs @@ -0,0 +1,2874 @@ +// +using System; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260522225951_GrowthOpsFeatures")] + partial class GrowthOpsFeatures + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ClockIn") + .HasColumnType("timestamp with time zone"); + + b.Property("ClockOut") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "Date") + .IsUnique(); + + b.ToTable("Attendances"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccentColor") + .HasColumnType("text"); + + b.Property("Address") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("AutoCutEnabled") + .HasColumnType("boolean"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("City") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("KitchenPrinterIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("KitchenPrinterPort") + .HasColumnType("integer"); + + b.Property("LogoUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("PaperWidthMm") + .HasColumnType("integer"); + + b.Property("Phone") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("PosDeviceIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("PosDevicePort") + .HasColumnType("integer"); + + b.Property("ReceiptFooter") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptHeader") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptPrinterIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("ReceiptPrinterPort") + .HasColumnType("integer"); + + b.Property("ScheduledPermanentDeleteAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TaxRate") + .HasColumnType("numeric"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WelcomeText") + .HasColumnType("text"); + + b.Property("WifiPassword") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "IsActive"); + + b.ToTable("Branches"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.BranchMenuItemOverride", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("PriceOverride") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("SortOrderOverride") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedByUserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("BranchId", "MenuItemId") + .IsUnique(); + + b.ToTable("BranchMenuItemOverrides"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("AllowBranchTaxOverride") + .HasColumnType("boolean"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("CoverImageUrl") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DefaultTaxRate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DigikalaVendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("DiscoverBadgesJson") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("DiscoverProfileJson") + .HasMaxLength(8000) + .HasColumnType("character varying(8000)"); + + b.Property("IsSuspended") + .HasColumnType("boolean"); + + b.Property("IsVerified") + .HasColumnType("boolean"); + + b.Property("LogoUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("PlanExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("PreferredLanguage") + .IsRequired() + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SnappfoodVendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Tap30VendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ThemeJson") + .HasMaxLength(8000) + .HasColumnType("character varying(8000)"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Cafes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeFeatureOverride", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FeatureKey") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "FeatureKey") + .IsUnique(); + + b.ToTable("CafeFeatureOverrides"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeNotification", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Body") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsRead") + .HasColumnType("boolean"); + + b.Property("ReadAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReferenceId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("TableNumber") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "IsRead", "CreatedAt"); + + b.ToTable("CafeNotifications"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AuthorName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("AuthorPhone") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Comment") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsHidden") + .HasColumnType("boolean"); + + b.Property("OwnerRepliedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerReply") + .HasColumnType("text"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "CreatedAt"); + + b.ToTable("CafeReviews"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReviewPhoto", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewId") + .IsRequired() + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("Url") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.HasKey("Id"); + + b.HasIndex("ReviewId"); + + b.ToTable("CafeReviewPhotos"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CashTransaction", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("ReferenceId") + .HasColumnType("text"); + + b.Property("ShiftId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("ShiftId"); + + b.HasIndex("CafeId", "BranchId"); + + b.ToTable("CashTransactions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.ConsumerAccount", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.HasKey("Id"); + + b.HasIndex("Phone") + .IsUnique(); + + b.ToTable("ConsumerAccounts"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MaxDiscount") + .HasColumnType("numeric"); + + b.Property("MinOrderAmount") + .HasColumnType("numeric"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TargetGroup") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UsageLimit") + .HasColumnType("integer"); + + b.Property("UsedCount") + .HasColumnType("integer"); + + b.Property("Value") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Code") + .IsUnique(); + + b.ToTable("Coupons"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BirthDateJalali") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Group") + .HasColumnType("integer"); + + b.Property("LoyaltyPoints") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReferredBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DailyReport", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AvgOrderValue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CardRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CashRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreditRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GeneratedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("NetIncome") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TopProducts") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("TotalExpenses") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TotalOrders") + .HasColumnType("integer"); + + b.Property("TotalRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TotalVoids") + .HasColumnType("integer"); + + b.Property("VoidAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "BranchId", "Date") + .IsUnique(); + + b.ToTable("DailyReports"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DeliveryCommissionRate", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Platform") + .HasColumnType("integer"); + + b.Property("RatePercent") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Platform") + .IsUnique(); + + b.ToTable("DeliveryCommissionRates"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("PinCode") + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "Phone") + .IsUnique() + .HasFilter("\"DeletedAt\" IS NULL"); + + b.ToTable("Employees"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Deductions") + .HasColumnType("numeric"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsPaid") + .HasColumnType("boolean"); + + b.Property("MonthYear") + .IsRequired() + .HasColumnType("text"); + + b.Property("NetSalary") + .HasColumnType("numeric"); + + b.Property("OvertimePay") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "MonthYear") + .IsUnique(); + + b.ToTable("EmployeeSalaries"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSchedule", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DayOfWeek") + .HasColumnType("integer"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShiftType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "DayOfWeek") + .IsUnique(); + + b.ToTable("EmployeeSchedules", (string)null); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Expense", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Category") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptImageUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ShiftId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("ShiftId"); + + b.HasIndex("CafeId", "BranchId", "CreatedAt"); + + b.ToTable("Expenses"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LowStockWarningPercent") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ParLevel") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("QuantityOnHand") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("ReorderLevel") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("Unit") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("UnitCost") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Ingredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.KitchenStation", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("PrinterIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("PrinterPort") + .HasColumnType("integer"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "SortOrder"); + + b.ToTable("KitchenStations"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndDate") + .HasColumnType("date"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("ReviewedBy") + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId"); + + b.ToTable("LeaveRequests"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("Icon") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("IconPresetId") + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("IconStyle") + .HasMaxLength(16) + .HasColumnType("character varying(16)"); + + b.Property("ImageUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("KitchenStationId") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("TaxId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("KitchenStationId"); + + b.HasIndex("TaxId"); + + b.ToTable("MenuCategories"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CategoryId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Model3dUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("CategoryId"); + + b.ToTable("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItemIngredient", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IngredientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("QuantityPerUnit") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.HasKey("Id"); + + b.HasIndex("IngredientId"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("CafeId", "MenuItemId", "IngredientId") + .IsUnique(); + + b.ToTable("MenuItemIngredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CouponId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeliveryMetaJson") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("DeliveryPlatform") + .HasColumnType("integer"); + + b.Property("DiscountAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("DisplayNumber") + .HasColumnType("integer"); + + b.Property("EmployeeId") + .HasColumnType("text"); + + b.Property("ExternalOrderId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("GuestName") + .HasColumnType("text"); + + b.Property("GuestPhone") + .HasColumnType("text"); + + b.Property("GuestTrackingToken") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("OrderType") + .HasColumnType("integer"); + + b.Property("PlatformCommission") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ReservationId") + .HasColumnType("text"); + + b.Property("SnappfoodOrderId") + .HasColumnType("text"); + + b.Property("Source") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("StatusUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Subtotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("TaxTotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Total") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CouponId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("GuestTrackingToken"); + + b.HasIndex("ReservationId"); + + b.HasIndex("TableId"); + + b.HasIndex("CafeId", "DisplayNumber") + .IsUnique(); + + b.HasIndex("CafeId", "DeliveryPlatform", "ExternalOrderId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsVoided") + .HasColumnType("boolean"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UnitPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VoidedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VoidedByUserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Reference") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.PlatformFeature", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DisplayNameEn") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DisplayNameFa") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("IsEnabledGlobally") + .HasColumnType("boolean"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("ModuleGroup") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("PlatformFeatures"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.PlatformPlanDefinition", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DisplayNameEn") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DisplayNameFa") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("FeaturesJson") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsBillableOnline") + .HasColumnType("boolean"); + + b.Property("LimitsJson") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("MonthlyPriceToman") + .HasPrecision(18) + .HasColumnType("numeric(18,0)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("Tier") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Tier") + .IsUnique(); + + b.ToTable("PlatformPlanDefinitions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.PlatformSetting", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DescriptionFa") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(8000) + .HasColumnType("character varying(8000)"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("PlatformSettings"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.QueueTicket", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerLabel") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedByUserId") + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("integer"); + + b.Property("OrderId") + .HasColumnType("text"); + + b.Property("ServiceDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("OrderId"); + + b.HasIndex("CafeId", "BranchId", "ServiceDate", "Number") + .IsUnique(); + + b.ToTable("QueueTickets"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClosedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ClosedByUserId") + .HasColumnType("text"); + + b.Property("ClosingCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Discrepancy") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ExpectedCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("OpenedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OpenedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("OpeningCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("ClosedByUserId"); + + b.HasIndex("OpenedByUserId"); + + b.HasIndex("BranchId", "Status"); + + b.ToTable("RegisterShifts", (string)null); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Delta") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("ExpenseId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("IngredientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Kind") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("OrderId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("TotalCostToman") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("IngredientId"); + + b.HasIndex("CafeId", "OrderId"); + + b.ToTable("StockMovements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AmountRials") + .HasColumnType("bigint"); + + b.Property("AmountToman") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Authority") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Months") + .HasColumnType("integer"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("Provider") + .HasColumnType("integer"); + + b.Property("RefId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Authority"); + + b.HasIndex("CafeId"); + + b.ToTable("SubscriptionPayments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicket", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AssignedAdminId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClosedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByEmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Priority") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subject") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedByEmployeeId"); + + b.HasIndex("CafeId", "Status", "UpdatedAt"); + + b.ToTable("SupportTickets"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicketMessage", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Body") + .IsRequired() + .HasMaxLength(8000) + .HasColumnType("character varying(8000)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SenderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("SenderKind") + .HasColumnType("integer"); + + b.Property("TicketId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("TicketId", "CreatedAt"); + + b.ToTable("SupportTicketMessages"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SystemAdmin", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.HasKey("Id"); + + b.HasIndex("Phone") + .IsUnique(); + + b.ToTable("SystemAdmins"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Floor") + .HasColumnType("text"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsCleaning") + .HasColumnType("boolean"); + + b.Property("Number") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("QrCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("SectionId") + .HasColumnType("text"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("QrCode") + .IsUnique(); + + b.HasIndex("SectionId"); + + b.HasIndex("BranchId", "SectionId", "SortOrder"); + + b.ToTable("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GuestName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GuestPhone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("PartySize") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("time without time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("TableId"); + + b.HasIndex("CafeId", "Date", "Time"); + + b.ToTable("TableReservations"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("BranchId", "Name"); + + b.ToTable("TableSections"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsCompound") + .HasColumnType("boolean"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("IsRequired") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.WebhookLog", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AttemptCount") + .HasColumnType("integer"); + + b.Property("CafeId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("ExternalOrderId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("MeeziOrderId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Platform") + .HasColumnType("integer"); + + b.Property("Processed") + .HasColumnType("boolean"); + + b.Property("ProcessedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RawBody") + .IsRequired() + .HasColumnType("text"); + + b.Property("SignatureHeader") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("SignatureValid") + .HasColumnType("boolean"); + + b.Property("Success") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("Platform", "CreatedAt"); + + b.ToTable("WebhookLogs"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Attendances") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Branches") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.BranchMenuItemOverride", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("BranchOverrides") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("MenuItem"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeFeatureOverride", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", null) + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Reviews") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReviewPhoto", b => + { + b.HasOne("Meezi.Core.Entities.CafeReview", "Review") + .WithMany("Photos") + .HasForeignKey("ReviewId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Review"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CashTransaction", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Shift", "Shift") + .WithMany("Transactions") + .HasForeignKey("ShiftId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Shift"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Coupons") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Customers") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DailyReport", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DeliveryCommissionRate", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Staff") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Employees") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Salaries") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSchedule", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Schedules") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Expense", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Shift", "Shift") + .WithMany() + .HasForeignKey("ShiftId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Shift"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Ingredients") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.KitchenStation", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("LeaveRequests") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuCategories") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.KitchenStation", "KitchenStation") + .WithMany("Categories") + .HasForeignKey("KitchenStationId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Tax", "Tax") + .WithMany("MenuCategories") + .HasForeignKey("TaxId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("KitchenStation"); + + b.Navigation("Tax"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuItems") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuCategory", "Category") + .WithMany("MenuItems") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItemIngredient", b => + { + b.HasOne("Meezi.Core.Entities.Ingredient", "Ingredient") + .WithMany("MenuItemRecipes") + .HasForeignKey("IngredientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("RecipeIngredients") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ingredient"); + + b.Navigation("MenuItem"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Orders") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Orders") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Coupon", "Coupon") + .WithMany("Orders") + .HasForeignKey("CouponId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Orders") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.TableReservation", "Reservation") + .WithMany() + .HasForeignKey("ReservationId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany("Orders") + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Coupon"); + + b.Navigation("Customer"); + + b.Navigation("Employee"); + + b.Navigation("Reservation"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("OrderItems") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Items") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MenuItem"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Payments") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.QueueTicket", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Employee", "ClosedBy") + .WithMany() + .HasForeignKey("ClosedByUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "OpenedBy") + .WithMany() + .HasForeignKey("OpenedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("ClosedBy"); + + b.Navigation("OpenedBy"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Ingredient", "Ingredient") + .WithMany("Movements") + .HasForeignKey("IngredientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Ingredient"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("SubscriptionPayments") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicket", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Employee", "CreatedByEmployee") + .WithMany() + .HasForeignKey("CreatedByEmployeeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("CreatedByEmployee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicketMessage", b => + { + b.HasOne("Meezi.Core.Entities.SupportTicket", "Ticket") + .WithMany("Messages") + .HasForeignKey("TicketId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ticket"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Tables") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Tables") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.TableSection", "Section") + .WithMany("Tables") + .HasForeignKey("SectionId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Section"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany() + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Customer"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Sections") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Taxes") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.WebhookLog", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Navigation("Orders"); + + b.Navigation("Sections"); + + b.Navigation("Staff"); + + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Navigation("Branches"); + + b.Navigation("Coupons"); + + b.Navigation("Customers"); + + b.Navigation("Employees"); + + b.Navigation("Ingredients"); + + b.Navigation("MenuCategories"); + + b.Navigation("MenuItems"); + + b.Navigation("Orders"); + + b.Navigation("Reviews"); + + b.Navigation("SubscriptionPayments"); + + b.Navigation("Tables"); + + b.Navigation("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.Navigation("Photos"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Navigation("Attendances"); + + b.Navigation("LeaveRequests"); + + b.Navigation("Orders"); + + b.Navigation("Salaries"); + + b.Navigation("Schedules"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Navigation("MenuItemRecipes"); + + b.Navigation("Movements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.KitchenStation", b => + { + b.Navigation("Categories"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Navigation("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Navigation("BranchOverrides"); + + b.Navigation("OrderItems"); + + b.Navigation("RecipeIngredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Navigation("Items"); + + b.Navigation("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicket", b => + { + b.Navigation("Messages"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Navigation("MenuCategories"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260522225951_GrowthOpsFeatures.cs b/src/Meezi.Infrastructure/Data/Migrations/20260522225951_GrowthOpsFeatures.cs new file mode 100644 index 0000000..2aaa4ea --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260522225951_GrowthOpsFeatures.cs @@ -0,0 +1,170 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + /// + public partial class GrowthOpsFeatures : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "KitchenStationId", + table: "MenuCategories", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "DiscoverBadgesJson", + table: "Cafes", + type: "character varying(2000)", + maxLength: 2000, + nullable: true); + + migrationBuilder.AddColumn( + name: "IsHidden", + table: "CafeReviews", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.CreateTable( + name: "CafeReviewPhotos", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + ReviewId = table.Column(type: "text", nullable: false), + Url = table.Column(type: "character varying(500)", maxLength: 500, nullable: false), + SortOrder = table.Column(type: "integer", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_CafeReviewPhotos", x => x.Id); + table.ForeignKey( + name: "FK_CafeReviewPhotos_CafeReviews_ReviewId", + column: x => x.ReviewId, + principalTable: "CafeReviews", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ConsumerAccounts", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + Phone = table.Column(type: "character varying(20)", maxLength: 20, nullable: false), + Name = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ConsumerAccounts", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "KitchenStations", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + BranchId = table.Column(type: "text", nullable: true), + Name = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + PrinterIp = table.Column(type: "character varying(45)", maxLength: 45, nullable: true), + PrinterPort = table.Column(type: "integer", nullable: false), + SortOrder = table.Column(type: "integer", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + CafeId = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_KitchenStations", x => x.Id); + table.ForeignKey( + name: "FK_KitchenStations_Branches_BranchId", + column: x => x.BranchId, + principalTable: "Branches", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "FK_KitchenStations_Cafes_CafeId", + column: x => x.CafeId, + principalTable: "Cafes", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_MenuCategories_KitchenStationId", + table: "MenuCategories", + column: "KitchenStationId"); + + migrationBuilder.CreateIndex( + name: "IX_CafeReviewPhotos_ReviewId", + table: "CafeReviewPhotos", + column: "ReviewId"); + + migrationBuilder.CreateIndex( + name: "IX_ConsumerAccounts_Phone", + table: "ConsumerAccounts", + column: "Phone", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_KitchenStations_BranchId", + table: "KitchenStations", + column: "BranchId"); + + migrationBuilder.CreateIndex( + name: "IX_KitchenStations_CafeId_SortOrder", + table: "KitchenStations", + columns: new[] { "CafeId", "SortOrder" }); + + migrationBuilder.AddForeignKey( + name: "FK_MenuCategories_KitchenStations_KitchenStationId", + table: "MenuCategories", + column: "KitchenStationId", + principalTable: "KitchenStations", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_MenuCategories_KitchenStations_KitchenStationId", + table: "MenuCategories"); + + migrationBuilder.DropTable( + name: "CafeReviewPhotos"); + + migrationBuilder.DropTable( + name: "ConsumerAccounts"); + + migrationBuilder.DropTable( + name: "KitchenStations"); + + migrationBuilder.DropIndex( + name: "IX_MenuCategories_KitchenStationId", + table: "MenuCategories"); + + migrationBuilder.DropColumn( + name: "KitchenStationId", + table: "MenuCategories"); + + migrationBuilder.DropColumn( + name: "DiscoverBadgesJson", + table: "Cafes"); + + migrationBuilder.DropColumn( + name: "IsHidden", + table: "CafeReviews"); + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260525204337_AddWebsiteCms.Designer.cs b/src/Meezi.Infrastructure/Data/Migrations/20260525204337_AddWebsiteCms.Designer.cs new file mode 100644 index 0000000..07560ed --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260525204337_AddWebsiteCms.Designer.cs @@ -0,0 +1,3087 @@ +// +using System; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260525204337_AddWebsiteCms")] + partial class AddWebsiteCms + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ClockIn") + .HasColumnType("timestamp with time zone"); + + b.Property("ClockOut") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "Date") + .IsUnique(); + + b.ToTable("Attendances"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccentColor") + .HasColumnType("text"); + + b.Property("Address") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("AutoCutEnabled") + .HasColumnType("boolean"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("City") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("KitchenPrinterIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("KitchenPrinterPort") + .HasColumnType("integer"); + + b.Property("LogoUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("PaperWidthMm") + .HasColumnType("integer"); + + b.Property("Phone") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("PosDeviceIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("PosDevicePort") + .HasColumnType("integer"); + + b.Property("ReceiptFooter") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptHeader") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptPrinterIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("ReceiptPrinterPort") + .HasColumnType("integer"); + + b.Property("ScheduledPermanentDeleteAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TaxRate") + .HasColumnType("numeric"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WelcomeText") + .HasColumnType("text"); + + b.Property("WifiPassword") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "IsActive"); + + b.ToTable("Branches"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.BranchMenuItemOverride", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("PriceOverride") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("SortOrderOverride") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedByUserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("BranchId", "MenuItemId") + .IsUnique(); + + b.ToTable("BranchMenuItemOverrides"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("AllowBranchTaxOverride") + .HasColumnType("boolean"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("CoverImageUrl") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DefaultTaxRate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DigikalaVendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("DiscoverBadgesJson") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("DiscoverProfileJson") + .HasMaxLength(8000) + .HasColumnType("character varying(8000)"); + + b.Property("IsSuspended") + .HasColumnType("boolean"); + + b.Property("IsVerified") + .HasColumnType("boolean"); + + b.Property("LogoUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("PlanExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("PreferredLanguage") + .IsRequired() + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SnappfoodVendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Tap30VendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ThemeJson") + .HasMaxLength(8000) + .HasColumnType("character varying(8000)"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Cafes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeFeatureOverride", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FeatureKey") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "FeatureKey") + .IsUnique(); + + b.ToTable("CafeFeatureOverrides"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeNotification", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Body") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsRead") + .HasColumnType("boolean"); + + b.Property("ReadAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReferenceId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("TableNumber") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "IsRead", "CreatedAt"); + + b.ToTable("CafeNotifications"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AuthorName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("AuthorPhone") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Comment") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsHidden") + .HasColumnType("boolean"); + + b.Property("OwnerRepliedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerReply") + .HasColumnType("text"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "CreatedAt"); + + b.ToTable("CafeReviews"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReviewPhoto", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewId") + .IsRequired() + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("Url") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.HasKey("Id"); + + b.HasIndex("ReviewId"); + + b.ToTable("CafeReviewPhotos"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CashTransaction", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("ReferenceId") + .HasColumnType("text"); + + b.Property("ShiftId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("ShiftId"); + + b.HasIndex("CafeId", "BranchId"); + + b.ToTable("CashTransactions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.ConsumerAccount", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.HasKey("Id"); + + b.HasIndex("Phone") + .IsUnique(); + + b.ToTable("ConsumerAccounts"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MaxDiscount") + .HasColumnType("numeric"); + + b.Property("MinOrderAmount") + .HasColumnType("numeric"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TargetGroup") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UsageLimit") + .HasColumnType("integer"); + + b.Property("UsedCount") + .HasColumnType("integer"); + + b.Property("Value") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Code") + .IsUnique(); + + b.ToTable("Coupons"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BirthDateJalali") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Group") + .HasColumnType("integer"); + + b.Property("LoyaltyPoints") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReferredBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DailyReport", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AvgOrderValue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CardRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CashRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreditRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GeneratedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("NetIncome") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TopProducts") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("TotalExpenses") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TotalOrders") + .HasColumnType("integer"); + + b.Property("TotalRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TotalVoids") + .HasColumnType("integer"); + + b.Property("VoidAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "BranchId", "Date") + .IsUnique(); + + b.ToTable("DailyReports"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DeliveryCommissionRate", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Platform") + .HasColumnType("integer"); + + b.Property("RatePercent") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Platform") + .IsUnique(); + + b.ToTable("DeliveryCommissionRates"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DemoRequest", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AdminNotes") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("BranchCount") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("BusinessName") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("ContactName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ContactedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Notes") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Source") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasDefaultValue("website"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Status", "CreatedAt"); + + b.ToTable("DemoRequests"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("PinCode") + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "Phone") + .IsUnique() + .HasFilter("\"DeletedAt\" IS NULL"); + + b.ToTable("Employees"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Deductions") + .HasColumnType("numeric"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsPaid") + .HasColumnType("boolean"); + + b.Property("MonthYear") + .IsRequired() + .HasColumnType("text"); + + b.Property("NetSalary") + .HasColumnType("numeric"); + + b.Property("OvertimePay") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "MonthYear") + .IsUnique(); + + b.ToTable("EmployeeSalaries"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSchedule", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DayOfWeek") + .HasColumnType("integer"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShiftType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "DayOfWeek") + .IsUnique(); + + b.ToTable("EmployeeSchedules", (string)null); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Expense", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Category") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptImageUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ShiftId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("ShiftId"); + + b.HasIndex("CafeId", "BranchId", "CreatedAt"); + + b.ToTable("Expenses"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LowStockWarningPercent") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ParLevel") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("QuantityOnHand") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("ReorderLevel") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("Unit") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("UnitCost") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Ingredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.KitchenStation", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("PrinterIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("PrinterPort") + .HasColumnType("integer"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "SortOrder"); + + b.ToTable("KitchenStations"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndDate") + .HasColumnType("date"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("ReviewedBy") + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId"); + + b.ToTable("LeaveRequests"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("Icon") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("IconPresetId") + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("IconStyle") + .HasMaxLength(16) + .HasColumnType("character varying(16)"); + + b.Property("ImageUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("KitchenStationId") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("TaxId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("KitchenStationId"); + + b.HasIndex("TaxId"); + + b.ToTable("MenuCategories"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CategoryId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Model3dUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("CategoryId"); + + b.ToTable("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItemIngredient", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IngredientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("QuantityPerUnit") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.HasKey("Id"); + + b.HasIndex("IngredientId"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("CafeId", "MenuItemId", "IngredientId") + .IsUnique(); + + b.ToTable("MenuItemIngredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CouponId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeliveryMetaJson") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("DeliveryPlatform") + .HasColumnType("integer"); + + b.Property("DiscountAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("DisplayNumber") + .HasColumnType("integer"); + + b.Property("EmployeeId") + .HasColumnType("text"); + + b.Property("ExternalOrderId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("GuestName") + .HasColumnType("text"); + + b.Property("GuestPhone") + .HasColumnType("text"); + + b.Property("GuestTrackingToken") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("OrderType") + .HasColumnType("integer"); + + b.Property("PlatformCommission") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ReservationId") + .HasColumnType("text"); + + b.Property("SnappfoodOrderId") + .HasColumnType("text"); + + b.Property("Source") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("StatusUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Subtotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("TaxTotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Total") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CouponId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("GuestTrackingToken"); + + b.HasIndex("ReservationId"); + + b.HasIndex("TableId"); + + b.HasIndex("CafeId", "DisplayNumber") + .IsUnique(); + + b.HasIndex("CafeId", "DeliveryPlatform", "ExternalOrderId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsVoided") + .HasColumnType("boolean"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UnitPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VoidedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VoidedByUserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Reference") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.PlatformFeature", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DisplayNameEn") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DisplayNameFa") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("IsEnabledGlobally") + .HasColumnType("boolean"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("ModuleGroup") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("PlatformFeatures"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.PlatformPlanDefinition", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DisplayNameEn") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DisplayNameFa") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("FeaturesJson") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsBillableOnline") + .HasColumnType("boolean"); + + b.Property("LimitsJson") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("MonthlyPriceToman") + .HasPrecision(18) + .HasColumnType("numeric(18,0)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("Tier") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Tier") + .IsUnique(); + + b.ToTable("PlatformPlanDefinitions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.PlatformSetting", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DescriptionFa") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(8000) + .HasColumnType("character varying(8000)"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("PlatformSettings"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.QueueTicket", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerLabel") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedByUserId") + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("integer"); + + b.Property("OrderId") + .HasColumnType("text"); + + b.Property("ServiceDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("OrderId"); + + b.HasIndex("CafeId", "BranchId", "ServiceDate", "Number") + .IsUnique(); + + b.ToTable("QueueTickets"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClosedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ClosedByUserId") + .HasColumnType("text"); + + b.Property("ClosingCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Discrepancy") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ExpectedCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("OpenedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OpenedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("OpeningCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("ClosedByUserId"); + + b.HasIndex("OpenedByUserId"); + + b.HasIndex("BranchId", "Status"); + + b.ToTable("RegisterShifts", (string)null); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Delta") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("ExpenseId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("IngredientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Kind") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("OrderId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("TotalCostToman") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("IngredientId"); + + b.HasIndex("CafeId", "OrderId"); + + b.ToTable("StockMovements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AmountRials") + .HasColumnType("bigint"); + + b.Property("AmountToman") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Authority") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Months") + .HasColumnType("integer"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("Provider") + .HasColumnType("integer"); + + b.Property("RefId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Authority"); + + b.HasIndex("CafeId"); + + b.ToTable("SubscriptionPayments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicket", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AssignedAdminId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClosedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByEmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Priority") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subject") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedByEmployeeId"); + + b.HasIndex("CafeId", "Status", "UpdatedAt"); + + b.ToTable("SupportTickets"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicketMessage", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Body") + .IsRequired() + .HasMaxLength(8000) + .HasColumnType("character varying(8000)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SenderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("SenderKind") + .HasColumnType("integer"); + + b.Property("TicketId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("TicketId", "CreatedAt"); + + b.ToTable("SupportTicketMessages"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SystemAdmin", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.HasKey("Id"); + + b.HasIndex("Phone") + .IsUnique(); + + b.ToTable("SystemAdmins"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Floor") + .HasColumnType("text"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsCleaning") + .HasColumnType("boolean"); + + b.Property("Number") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("QrCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("SectionId") + .HasColumnType("text"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("QrCode") + .IsUnique(); + + b.HasIndex("SectionId"); + + b.HasIndex("BranchId", "SectionId", "SortOrder"); + + b.ToTable("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GuestName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GuestPhone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("PartySize") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("time without time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("TableId"); + + b.HasIndex("CafeId", "Date", "Time"); + + b.ToTable("TableReservations"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("BranchId", "Name"); + + b.ToTable("TableSections"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsCompound") + .HasColumnType("boolean"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("IsRequired") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.WebhookLog", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AttemptCount") + .HasColumnType("integer"); + + b.Property("CafeId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("ExternalOrderId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("MeeziOrderId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Platform") + .HasColumnType("integer"); + + b.Property("Processed") + .HasColumnType("boolean"); + + b.Property("ProcessedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RawBody") + .IsRequired() + .HasColumnType("text"); + + b.Property("SignatureHeader") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("SignatureValid") + .HasColumnType("boolean"); + + b.Property("Success") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("Platform", "CreatedAt"); + + b.ToTable("WebhookLogs"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.WebsiteBlogPost", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Author") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("CategoryEn") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CategoryFa") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ContentEn") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContentFa") + .IsRequired() + .HasColumnType("text"); + + b.Property("CoverImage") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExcerptEn") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("ExcerptFa") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("IsPublished") + .HasColumnType("boolean"); + + b.Property("PublishedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("TagsJson") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasDefaultValue("[]"); + + b.Property("TitleEn") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("TitleFa") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("ViewCount") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.HasIndex("IsPublished", "PublishedAt"); + + b.ToTable("WebsiteBlogPosts"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.WebsiteComment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AuthorEmail") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("AuthorName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(3000) + .HasColumnType("character varying(3000)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsApproved") + .HasColumnType("boolean"); + + b.Property("PostSlug") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.HasKey("Id"); + + b.HasIndex("PostSlug", "IsApproved", "CreatedAt"); + + b.ToTable("WebsiteComments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Attendances") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Branches") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.BranchMenuItemOverride", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("BranchOverrides") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("MenuItem"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeFeatureOverride", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", null) + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Reviews") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReviewPhoto", b => + { + b.HasOne("Meezi.Core.Entities.CafeReview", "Review") + .WithMany("Photos") + .HasForeignKey("ReviewId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Review"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CashTransaction", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Shift", "Shift") + .WithMany("Transactions") + .HasForeignKey("ShiftId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Shift"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Coupons") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Customers") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DailyReport", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DeliveryCommissionRate", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Staff") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Employees") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Salaries") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSchedule", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Schedules") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Expense", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Shift", "Shift") + .WithMany() + .HasForeignKey("ShiftId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Shift"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Ingredients") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.KitchenStation", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("LeaveRequests") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuCategories") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.KitchenStation", "KitchenStation") + .WithMany("Categories") + .HasForeignKey("KitchenStationId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Tax", "Tax") + .WithMany("MenuCategories") + .HasForeignKey("TaxId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("KitchenStation"); + + b.Navigation("Tax"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuItems") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuCategory", "Category") + .WithMany("MenuItems") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItemIngredient", b => + { + b.HasOne("Meezi.Core.Entities.Ingredient", "Ingredient") + .WithMany("MenuItemRecipes") + .HasForeignKey("IngredientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("RecipeIngredients") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ingredient"); + + b.Navigation("MenuItem"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Orders") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Orders") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Coupon", "Coupon") + .WithMany("Orders") + .HasForeignKey("CouponId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Orders") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.TableReservation", "Reservation") + .WithMany() + .HasForeignKey("ReservationId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany("Orders") + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Coupon"); + + b.Navigation("Customer"); + + b.Navigation("Employee"); + + b.Navigation("Reservation"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("OrderItems") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Items") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MenuItem"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Payments") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.QueueTicket", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Employee", "ClosedBy") + .WithMany() + .HasForeignKey("ClosedByUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "OpenedBy") + .WithMany() + .HasForeignKey("OpenedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("ClosedBy"); + + b.Navigation("OpenedBy"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Ingredient", "Ingredient") + .WithMany("Movements") + .HasForeignKey("IngredientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Ingredient"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("SubscriptionPayments") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicket", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Employee", "CreatedByEmployee") + .WithMany() + .HasForeignKey("CreatedByEmployeeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("CreatedByEmployee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicketMessage", b => + { + b.HasOne("Meezi.Core.Entities.SupportTicket", "Ticket") + .WithMany("Messages") + .HasForeignKey("TicketId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ticket"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Tables") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Tables") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.TableSection", "Section") + .WithMany("Tables") + .HasForeignKey("SectionId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Section"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany() + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Customer"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Sections") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Taxes") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.WebhookLog", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.WebsiteComment", b => + { + b.HasOne("Meezi.Core.Entities.WebsiteBlogPost", "Post") + .WithMany("Comments") + .HasForeignKey("PostSlug") + .HasPrincipalKey("Slug") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Post"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Navigation("Orders"); + + b.Navigation("Sections"); + + b.Navigation("Staff"); + + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Navigation("Branches"); + + b.Navigation("Coupons"); + + b.Navigation("Customers"); + + b.Navigation("Employees"); + + b.Navigation("Ingredients"); + + b.Navigation("MenuCategories"); + + b.Navigation("MenuItems"); + + b.Navigation("Orders"); + + b.Navigation("Reviews"); + + b.Navigation("SubscriptionPayments"); + + b.Navigation("Tables"); + + b.Navigation("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.Navigation("Photos"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Navigation("Attendances"); + + b.Navigation("LeaveRequests"); + + b.Navigation("Orders"); + + b.Navigation("Salaries"); + + b.Navigation("Schedules"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Navigation("MenuItemRecipes"); + + b.Navigation("Movements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.KitchenStation", b => + { + b.Navigation("Categories"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Navigation("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Navigation("BranchOverrides"); + + b.Navigation("OrderItems"); + + b.Navigation("RecipeIngredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Navigation("Items"); + + b.Navigation("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicket", b => + { + b.Navigation("Messages"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Navigation("MenuCategories"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.WebsiteBlogPost", b => + { + b.Navigation("Comments"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260525204337_AddWebsiteCms.cs b/src/Meezi.Infrastructure/Data/Migrations/20260525204337_AddWebsiteCms.cs new file mode 100644 index 0000000..1304e25 --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260525204337_AddWebsiteCms.cs @@ -0,0 +1,126 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + /// + public partial class AddWebsiteCms : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "DemoRequests", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + ContactName = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + BusinessName = table.Column(type: "character varying(300)", maxLength: 300, nullable: false), + Phone = table.Column(type: "character varying(20)", maxLength: 20, nullable: false), + Email = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + BranchCount = table.Column(type: "character varying(20)", maxLength: 20, nullable: false), + Notes = table.Column(type: "character varying(2000)", maxLength: 2000, nullable: true), + Source = table.Column(type: "character varying(50)", maxLength: 50, nullable: false, defaultValue: "website"), + Status = table.Column(type: "integer", nullable: false), + AdminNotes = table.Column(type: "character varying(2000)", maxLength: 2000, nullable: true), + ContactedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_DemoRequests", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "WebsiteBlogPosts", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + Slug = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + TitleFa = table.Column(type: "character varying(400)", maxLength: 400, nullable: false), + TitleEn = table.Column(type: "character varying(400)", maxLength: 400, nullable: false), + ExcerptFa = table.Column(type: "character varying(1000)", maxLength: 1000, nullable: false), + ExcerptEn = table.Column(type: "character varying(1000)", maxLength: 1000, nullable: false), + ContentFa = table.Column(type: "text", nullable: false), + ContentEn = table.Column(type: "text", nullable: false), + Author = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + CategoryFa = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + CategoryEn = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + TagsJson = table.Column(type: "character varying(2000)", maxLength: 2000, nullable: false, defaultValue: "[]"), + CoverImage = table.Column(type: "character varying(500)", maxLength: 500, nullable: true), + IsPublished = table.Column(type: "boolean", nullable: false), + PublishedAt = table.Column(type: "timestamp with time zone", nullable: true), + ViewCount = table.Column(type: "integer", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_WebsiteBlogPosts", x => x.Id); + table.UniqueConstraint("AK_WebsiteBlogPosts_Slug", x => x.Slug); + }); + + migrationBuilder.CreateTable( + name: "WebsiteComments", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + PostSlug = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + AuthorName = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + AuthorEmail = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + Content = table.Column(type: "character varying(3000)", maxLength: 3000, nullable: false), + IsApproved = table.Column(type: "boolean", nullable: false), + IpAddress = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_WebsiteComments", x => x.Id); + table.ForeignKey( + name: "FK_WebsiteComments_WebsiteBlogPosts_PostSlug", + column: x => x.PostSlug, + principalTable: "WebsiteBlogPosts", + principalColumn: "Slug", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_DemoRequests_Status_CreatedAt", + table: "DemoRequests", + columns: new[] { "Status", "CreatedAt" }); + + migrationBuilder.CreateIndex( + name: "IX_WebsiteBlogPosts_IsPublished_PublishedAt", + table: "WebsiteBlogPosts", + columns: new[] { "IsPublished", "PublishedAt" }); + + migrationBuilder.CreateIndex( + name: "IX_WebsiteBlogPosts_Slug", + table: "WebsiteBlogPosts", + column: "Slug", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_WebsiteComments_PostSlug_IsApproved_CreatedAt", + table: "WebsiteComments", + columns: new[] { "PostSlug", "IsApproved", "CreatedAt" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "DemoRequests"); + + migrationBuilder.DropTable( + name: "WebsiteComments"); + + migrationBuilder.DropTable( + name: "WebsiteBlogPosts"); + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260526164132_AddCafePublicProfile.Designer.cs b/src/Meezi.Infrastructure/Data/Migrations/20260526164132_AddCafePublicProfile.Designer.cs new file mode 100644 index 0000000..21d07ea --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260526164132_AddCafePublicProfile.Designer.cs @@ -0,0 +1,3099 @@ +// +using System; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260526164132_AddCafePublicProfile")] + partial class AddCafePublicProfile + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ClockIn") + .HasColumnType("timestamp with time zone"); + + b.Property("ClockOut") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "Date") + .IsUnique(); + + b.ToTable("Attendances"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccentColor") + .HasColumnType("text"); + + b.Property("Address") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("AutoCutEnabled") + .HasColumnType("boolean"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("City") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("KitchenPrinterIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("KitchenPrinterPort") + .HasColumnType("integer"); + + b.Property("LogoUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("PaperWidthMm") + .HasColumnType("integer"); + + b.Property("Phone") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("PosDeviceIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("PosDevicePort") + .HasColumnType("integer"); + + b.Property("ReceiptFooter") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptHeader") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptPrinterIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("ReceiptPrinterPort") + .HasColumnType("integer"); + + b.Property("ScheduledPermanentDeleteAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TaxRate") + .HasColumnType("numeric"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WelcomeText") + .HasColumnType("text"); + + b.Property("WifiPassword") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "IsActive"); + + b.ToTable("Branches"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.BranchMenuItemOverride", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("PriceOverride") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("SortOrderOverride") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedByUserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("BranchId", "MenuItemId") + .IsUnique(); + + b.ToTable("BranchMenuItemOverrides"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("AllowBranchTaxOverride") + .HasColumnType("boolean"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("CoverImageUrl") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DefaultTaxRate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DigikalaVendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("DiscoverBadgesJson") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("DiscoverProfileJson") + .HasMaxLength(8000) + .HasColumnType("character varying(8000)"); + + b.Property("GalleryJson") + .HasColumnType("text"); + + b.Property("InstagramHandle") + .HasColumnType("text"); + + b.Property("IsSuspended") + .HasColumnType("boolean"); + + b.Property("IsVerified") + .HasColumnType("boolean"); + + b.Property("LogoUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("PlanExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("PreferredLanguage") + .IsRequired() + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SnappfoodVendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Tap30VendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ThemeJson") + .HasMaxLength(8000) + .HasColumnType("character varying(8000)"); + + b.Property("WebsiteUrl") + .HasColumnType("text"); + + b.Property("WorkingHoursJson") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Cafes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeFeatureOverride", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FeatureKey") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "FeatureKey") + .IsUnique(); + + b.ToTable("CafeFeatureOverrides"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeNotification", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Body") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsRead") + .HasColumnType("boolean"); + + b.Property("ReadAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReferenceId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("TableNumber") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "IsRead", "CreatedAt"); + + b.ToTable("CafeNotifications"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AuthorName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("AuthorPhone") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Comment") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsHidden") + .HasColumnType("boolean"); + + b.Property("OwnerRepliedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerReply") + .HasColumnType("text"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "CreatedAt"); + + b.ToTable("CafeReviews"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReviewPhoto", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewId") + .IsRequired() + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("Url") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.HasKey("Id"); + + b.HasIndex("ReviewId"); + + b.ToTable("CafeReviewPhotos"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CashTransaction", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("ReferenceId") + .HasColumnType("text"); + + b.Property("ShiftId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("ShiftId"); + + b.HasIndex("CafeId", "BranchId"); + + b.ToTable("CashTransactions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.ConsumerAccount", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.HasKey("Id"); + + b.HasIndex("Phone") + .IsUnique(); + + b.ToTable("ConsumerAccounts"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MaxDiscount") + .HasColumnType("numeric"); + + b.Property("MinOrderAmount") + .HasColumnType("numeric"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TargetGroup") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UsageLimit") + .HasColumnType("integer"); + + b.Property("UsedCount") + .HasColumnType("integer"); + + b.Property("Value") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Code") + .IsUnique(); + + b.ToTable("Coupons"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BirthDateJalali") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Group") + .HasColumnType("integer"); + + b.Property("LoyaltyPoints") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReferredBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DailyReport", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AvgOrderValue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CardRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CashRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreditRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GeneratedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("NetIncome") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TopProducts") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("TotalExpenses") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TotalOrders") + .HasColumnType("integer"); + + b.Property("TotalRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TotalVoids") + .HasColumnType("integer"); + + b.Property("VoidAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "BranchId", "Date") + .IsUnique(); + + b.ToTable("DailyReports"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DeliveryCommissionRate", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Platform") + .HasColumnType("integer"); + + b.Property("RatePercent") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Platform") + .IsUnique(); + + b.ToTable("DeliveryCommissionRates"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DemoRequest", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AdminNotes") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("BranchCount") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("BusinessName") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("ContactName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ContactedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Notes") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Source") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasDefaultValue("website"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Status", "CreatedAt"); + + b.ToTable("DemoRequests"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("PinCode") + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "Phone") + .IsUnique() + .HasFilter("\"DeletedAt\" IS NULL"); + + b.ToTable("Employees"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Deductions") + .HasColumnType("numeric"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsPaid") + .HasColumnType("boolean"); + + b.Property("MonthYear") + .IsRequired() + .HasColumnType("text"); + + b.Property("NetSalary") + .HasColumnType("numeric"); + + b.Property("OvertimePay") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "MonthYear") + .IsUnique(); + + b.ToTable("EmployeeSalaries"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSchedule", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DayOfWeek") + .HasColumnType("integer"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShiftType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "DayOfWeek") + .IsUnique(); + + b.ToTable("EmployeeSchedules", (string)null); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Expense", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Category") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptImageUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ShiftId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("ShiftId"); + + b.HasIndex("CafeId", "BranchId", "CreatedAt"); + + b.ToTable("Expenses"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LowStockWarningPercent") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ParLevel") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("QuantityOnHand") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("ReorderLevel") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("Unit") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("UnitCost") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Ingredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.KitchenStation", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("PrinterIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("PrinterPort") + .HasColumnType("integer"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "SortOrder"); + + b.ToTable("KitchenStations"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndDate") + .HasColumnType("date"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("ReviewedBy") + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId"); + + b.ToTable("LeaveRequests"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("Icon") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("IconPresetId") + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("IconStyle") + .HasMaxLength(16) + .HasColumnType("character varying(16)"); + + b.Property("ImageUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("KitchenStationId") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("TaxId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("KitchenStationId"); + + b.HasIndex("TaxId"); + + b.ToTable("MenuCategories"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CategoryId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Model3dUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("CategoryId"); + + b.ToTable("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItemIngredient", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IngredientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("QuantityPerUnit") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.HasKey("Id"); + + b.HasIndex("IngredientId"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("CafeId", "MenuItemId", "IngredientId") + .IsUnique(); + + b.ToTable("MenuItemIngredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CouponId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeliveryMetaJson") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("DeliveryPlatform") + .HasColumnType("integer"); + + b.Property("DiscountAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("DisplayNumber") + .HasColumnType("integer"); + + b.Property("EmployeeId") + .HasColumnType("text"); + + b.Property("ExternalOrderId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("GuestName") + .HasColumnType("text"); + + b.Property("GuestPhone") + .HasColumnType("text"); + + b.Property("GuestTrackingToken") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("OrderType") + .HasColumnType("integer"); + + b.Property("PlatformCommission") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ReservationId") + .HasColumnType("text"); + + b.Property("SnappfoodOrderId") + .HasColumnType("text"); + + b.Property("Source") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("StatusUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Subtotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("TaxTotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Total") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CouponId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("GuestTrackingToken"); + + b.HasIndex("ReservationId"); + + b.HasIndex("TableId"); + + b.HasIndex("CafeId", "DisplayNumber") + .IsUnique(); + + b.HasIndex("CafeId", "DeliveryPlatform", "ExternalOrderId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsVoided") + .HasColumnType("boolean"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UnitPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VoidedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VoidedByUserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Reference") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.PlatformFeature", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DisplayNameEn") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DisplayNameFa") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("IsEnabledGlobally") + .HasColumnType("boolean"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("ModuleGroup") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("PlatformFeatures"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.PlatformPlanDefinition", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DisplayNameEn") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DisplayNameFa") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("FeaturesJson") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsBillableOnline") + .HasColumnType("boolean"); + + b.Property("LimitsJson") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("MonthlyPriceToman") + .HasPrecision(18) + .HasColumnType("numeric(18,0)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("Tier") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Tier") + .IsUnique(); + + b.ToTable("PlatformPlanDefinitions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.PlatformSetting", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DescriptionFa") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(8000) + .HasColumnType("character varying(8000)"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("PlatformSettings"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.QueueTicket", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerLabel") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedByUserId") + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("integer"); + + b.Property("OrderId") + .HasColumnType("text"); + + b.Property("ServiceDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("OrderId"); + + b.HasIndex("CafeId", "BranchId", "ServiceDate", "Number") + .IsUnique(); + + b.ToTable("QueueTickets"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClosedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ClosedByUserId") + .HasColumnType("text"); + + b.Property("ClosingCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Discrepancy") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ExpectedCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("OpenedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OpenedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("OpeningCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("ClosedByUserId"); + + b.HasIndex("OpenedByUserId"); + + b.HasIndex("BranchId", "Status"); + + b.ToTable("RegisterShifts", (string)null); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Delta") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("ExpenseId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("IngredientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Kind") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("OrderId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("TotalCostToman") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("IngredientId"); + + b.HasIndex("CafeId", "OrderId"); + + b.ToTable("StockMovements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AmountRials") + .HasColumnType("bigint"); + + b.Property("AmountToman") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Authority") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Months") + .HasColumnType("integer"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("Provider") + .HasColumnType("integer"); + + b.Property("RefId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Authority"); + + b.HasIndex("CafeId"); + + b.ToTable("SubscriptionPayments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicket", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AssignedAdminId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClosedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByEmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Priority") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subject") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedByEmployeeId"); + + b.HasIndex("CafeId", "Status", "UpdatedAt"); + + b.ToTable("SupportTickets"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicketMessage", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Body") + .IsRequired() + .HasMaxLength(8000) + .HasColumnType("character varying(8000)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SenderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("SenderKind") + .HasColumnType("integer"); + + b.Property("TicketId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("TicketId", "CreatedAt"); + + b.ToTable("SupportTicketMessages"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SystemAdmin", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.HasKey("Id"); + + b.HasIndex("Phone") + .IsUnique(); + + b.ToTable("SystemAdmins"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Floor") + .HasColumnType("text"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsCleaning") + .HasColumnType("boolean"); + + b.Property("Number") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("QrCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("SectionId") + .HasColumnType("text"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("QrCode") + .IsUnique(); + + b.HasIndex("SectionId"); + + b.HasIndex("BranchId", "SectionId", "SortOrder"); + + b.ToTable("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GuestName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GuestPhone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("PartySize") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("time without time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("TableId"); + + b.HasIndex("CafeId", "Date", "Time"); + + b.ToTable("TableReservations"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("BranchId", "Name"); + + b.ToTable("TableSections"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsCompound") + .HasColumnType("boolean"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("IsRequired") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.WebhookLog", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AttemptCount") + .HasColumnType("integer"); + + b.Property("CafeId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("ExternalOrderId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("MeeziOrderId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Platform") + .HasColumnType("integer"); + + b.Property("Processed") + .HasColumnType("boolean"); + + b.Property("ProcessedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RawBody") + .IsRequired() + .HasColumnType("text"); + + b.Property("SignatureHeader") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("SignatureValid") + .HasColumnType("boolean"); + + b.Property("Success") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("Platform", "CreatedAt"); + + b.ToTable("WebhookLogs"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.WebsiteBlogPost", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Author") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("CategoryEn") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CategoryFa") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ContentEn") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContentFa") + .IsRequired() + .HasColumnType("text"); + + b.Property("CoverImage") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExcerptEn") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("ExcerptFa") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("IsPublished") + .HasColumnType("boolean"); + + b.Property("PublishedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("TagsJson") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasDefaultValue("[]"); + + b.Property("TitleEn") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("TitleFa") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("ViewCount") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.HasIndex("IsPublished", "PublishedAt"); + + b.ToTable("WebsiteBlogPosts"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.WebsiteComment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AuthorEmail") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("AuthorName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(3000) + .HasColumnType("character varying(3000)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsApproved") + .HasColumnType("boolean"); + + b.Property("PostSlug") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.HasKey("Id"); + + b.HasIndex("PostSlug", "IsApproved", "CreatedAt"); + + b.ToTable("WebsiteComments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Attendances") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Branches") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.BranchMenuItemOverride", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("BranchOverrides") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("MenuItem"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeFeatureOverride", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", null) + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Reviews") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReviewPhoto", b => + { + b.HasOne("Meezi.Core.Entities.CafeReview", "Review") + .WithMany("Photos") + .HasForeignKey("ReviewId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Review"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CashTransaction", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Shift", "Shift") + .WithMany("Transactions") + .HasForeignKey("ShiftId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Shift"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Coupons") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Customers") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DailyReport", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DeliveryCommissionRate", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Staff") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Employees") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Salaries") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSchedule", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Schedules") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Expense", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Shift", "Shift") + .WithMany() + .HasForeignKey("ShiftId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Shift"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Ingredients") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.KitchenStation", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("LeaveRequests") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuCategories") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.KitchenStation", "KitchenStation") + .WithMany("Categories") + .HasForeignKey("KitchenStationId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Tax", "Tax") + .WithMany("MenuCategories") + .HasForeignKey("TaxId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("KitchenStation"); + + b.Navigation("Tax"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuItems") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuCategory", "Category") + .WithMany("MenuItems") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItemIngredient", b => + { + b.HasOne("Meezi.Core.Entities.Ingredient", "Ingredient") + .WithMany("MenuItemRecipes") + .HasForeignKey("IngredientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("RecipeIngredients") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ingredient"); + + b.Navigation("MenuItem"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Orders") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Orders") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Coupon", "Coupon") + .WithMany("Orders") + .HasForeignKey("CouponId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Orders") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.TableReservation", "Reservation") + .WithMany() + .HasForeignKey("ReservationId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany("Orders") + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Coupon"); + + b.Navigation("Customer"); + + b.Navigation("Employee"); + + b.Navigation("Reservation"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("OrderItems") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Items") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MenuItem"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Payments") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.QueueTicket", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Employee", "ClosedBy") + .WithMany() + .HasForeignKey("ClosedByUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "OpenedBy") + .WithMany() + .HasForeignKey("OpenedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("ClosedBy"); + + b.Navigation("OpenedBy"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Ingredient", "Ingredient") + .WithMany("Movements") + .HasForeignKey("IngredientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Ingredient"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("SubscriptionPayments") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicket", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Employee", "CreatedByEmployee") + .WithMany() + .HasForeignKey("CreatedByEmployeeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("CreatedByEmployee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicketMessage", b => + { + b.HasOne("Meezi.Core.Entities.SupportTicket", "Ticket") + .WithMany("Messages") + .HasForeignKey("TicketId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ticket"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Tables") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Tables") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.TableSection", "Section") + .WithMany("Tables") + .HasForeignKey("SectionId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Section"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany() + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Customer"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Sections") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Taxes") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.WebhookLog", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.WebsiteComment", b => + { + b.HasOne("Meezi.Core.Entities.WebsiteBlogPost", "Post") + .WithMany("Comments") + .HasForeignKey("PostSlug") + .HasPrincipalKey("Slug") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Post"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Navigation("Orders"); + + b.Navigation("Sections"); + + b.Navigation("Staff"); + + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Navigation("Branches"); + + b.Navigation("Coupons"); + + b.Navigation("Customers"); + + b.Navigation("Employees"); + + b.Navigation("Ingredients"); + + b.Navigation("MenuCategories"); + + b.Navigation("MenuItems"); + + b.Navigation("Orders"); + + b.Navigation("Reviews"); + + b.Navigation("SubscriptionPayments"); + + b.Navigation("Tables"); + + b.Navigation("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.Navigation("Photos"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Navigation("Attendances"); + + b.Navigation("LeaveRequests"); + + b.Navigation("Orders"); + + b.Navigation("Salaries"); + + b.Navigation("Schedules"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Navigation("MenuItemRecipes"); + + b.Navigation("Movements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.KitchenStation", b => + { + b.Navigation("Categories"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Navigation("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Navigation("BranchOverrides"); + + b.Navigation("OrderItems"); + + b.Navigation("RecipeIngredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Navigation("Items"); + + b.Navigation("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicket", b => + { + b.Navigation("Messages"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Navigation("MenuCategories"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.WebsiteBlogPost", b => + { + b.Navigation("Comments"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/20260526164132_AddCafePublicProfile.cs b/src/Meezi.Infrastructure/Data/Migrations/20260526164132_AddCafePublicProfile.cs new file mode 100644 index 0000000..9c84e59 --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/20260526164132_AddCafePublicProfile.cs @@ -0,0 +1,58 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + /// + public partial class AddCafePublicProfile : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "GalleryJson", + table: "Cafes", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "InstagramHandle", + table: "Cafes", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "WebsiteUrl", + table: "Cafes", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "WorkingHoursJson", + table: "Cafes", + type: "text", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "GalleryJson", + table: "Cafes"); + + migrationBuilder.DropColumn( + name: "InstagramHandle", + table: "Cafes"); + + migrationBuilder.DropColumn( + name: "WebsiteUrl", + table: "Cafes"); + + migrationBuilder.DropColumn( + name: "WorkingHoursJson", + table: "Cafes"); + } + } +} diff --git a/src/Meezi.Infrastructure/Data/Migrations/AppDbContextModelSnapshot.cs b/src/Meezi.Infrastructure/Data/Migrations/AppDbContextModelSnapshot.cs new file mode 100644 index 0000000..8e97149 --- /dev/null +++ b/src/Meezi.Infrastructure/Data/Migrations/AppDbContextModelSnapshot.cs @@ -0,0 +1,3096 @@ +// +using System; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Meezi.Infrastructure.Data.Migrations +{ + [DbContext(typeof(AppDbContext))] + partial class AppDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ClockIn") + .HasColumnType("timestamp with time zone"); + + b.Property("ClockOut") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "Date") + .IsUnique(); + + b.ToTable("Attendances"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccentColor") + .HasColumnType("text"); + + b.Property("Address") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("AutoCutEnabled") + .HasColumnType("boolean"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("City") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("KitchenPrinterIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("KitchenPrinterPort") + .HasColumnType("integer"); + + b.Property("LogoUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("PaperWidthMm") + .HasColumnType("integer"); + + b.Property("Phone") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("PosDeviceIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("PosDevicePort") + .HasColumnType("integer"); + + b.Property("ReceiptFooter") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptHeader") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptPrinterIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("ReceiptPrinterPort") + .HasColumnType("integer"); + + b.Property("ScheduledPermanentDeleteAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TaxRate") + .HasColumnType("numeric"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WelcomeText") + .HasColumnType("text"); + + b.Property("WifiPassword") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "IsActive"); + + b.ToTable("Branches"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.BranchMenuItemOverride", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("PriceOverride") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("SortOrderOverride") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedByUserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("BranchId", "MenuItemId") + .IsUnique(); + + b.ToTable("BranchMenuItemOverrides"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("AllowBranchTaxOverride") + .HasColumnType("boolean"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("CoverImageUrl") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DefaultTaxRate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DigikalaVendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("DiscoverBadgesJson") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("DiscoverProfileJson") + .HasMaxLength(8000) + .HasColumnType("character varying(8000)"); + + b.Property("GalleryJson") + .HasColumnType("text"); + + b.Property("InstagramHandle") + .HasColumnType("text"); + + b.Property("IsSuspended") + .HasColumnType("boolean"); + + b.Property("IsVerified") + .HasColumnType("boolean"); + + b.Property("LogoUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("PlanExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("PreferredLanguage") + .IsRequired() + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SnappfoodVendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Tap30VendorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ThemeJson") + .HasMaxLength(8000) + .HasColumnType("character varying(8000)"); + + b.Property("WebsiteUrl") + .HasColumnType("text"); + + b.Property("WorkingHoursJson") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Cafes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeFeatureOverride", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FeatureKey") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "FeatureKey") + .IsUnique(); + + b.ToTable("CafeFeatureOverrides"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeNotification", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Body") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsRead") + .HasColumnType("boolean"); + + b.Property("ReadAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReferenceId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("TableNumber") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "IsRead", "CreatedAt"); + + b.ToTable("CafeNotifications"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AuthorName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("AuthorPhone") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Comment") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsHidden") + .HasColumnType("boolean"); + + b.Property("OwnerRepliedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerReply") + .HasColumnType("text"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "CreatedAt"); + + b.ToTable("CafeReviews"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReviewPhoto", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewId") + .IsRequired() + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("Url") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.HasKey("Id"); + + b.HasIndex("ReviewId"); + + b.ToTable("CafeReviewPhotos"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CashTransaction", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("ReferenceId") + .HasColumnType("text"); + + b.Property("ShiftId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("ShiftId"); + + b.HasIndex("CafeId", "BranchId"); + + b.ToTable("CashTransactions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.ConsumerAccount", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.HasKey("Id"); + + b.HasIndex("Phone") + .IsUnique(); + + b.ToTable("ConsumerAccounts"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MaxDiscount") + .HasColumnType("numeric"); + + b.Property("MinOrderAmount") + .HasColumnType("numeric"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TargetGroup") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UsageLimit") + .HasColumnType("integer"); + + b.Property("UsedCount") + .HasColumnType("integer"); + + b.Property("Value") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Code") + .IsUnique(); + + b.ToTable("Coupons"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BirthDateJalali") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Group") + .HasColumnType("integer"); + + b.Property("LoyaltyPoints") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReferredBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Phone"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DailyReport", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AvgOrderValue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CardRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CashRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreditRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GeneratedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("NetIncome") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TopProducts") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("TotalExpenses") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TotalOrders") + .HasColumnType("integer"); + + b.Property("TotalRevenue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TotalVoids") + .HasColumnType("integer"); + + b.Property("VoidAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "BranchId", "Date") + .IsUnique(); + + b.ToTable("DailyReports"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DeliveryCommissionRate", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Platform") + .HasColumnType("integer"); + + b.Property("RatePercent") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CafeId", "Platform") + .IsUnique(); + + b.ToTable("DeliveryCommissionRates"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DemoRequest", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AdminNotes") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("BranchCount") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("BusinessName") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("ContactName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ContactedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Notes") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Source") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasDefaultValue("website"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Status", "CreatedAt"); + + b.ToTable("DemoRequests"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NationalId") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("PinCode") + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "Phone") + .IsUnique() + .HasFilter("\"DeletedAt\" IS NULL"); + + b.ToTable("Employees"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaseSalary") + .HasColumnType("numeric"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Deductions") + .HasColumnType("numeric"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsPaid") + .HasColumnType("boolean"); + + b.Property("MonthYear") + .IsRequired() + .HasColumnType("text"); + + b.Property("NetSalary") + .HasColumnType("numeric"); + + b.Property("OvertimePay") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "MonthYear") + .IsUnique(); + + b.ToTable("EmployeeSalaries"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSchedule", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DayOfWeek") + .HasColumnType("integer"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShiftType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId", "DayOfWeek") + .IsUnique(); + + b.ToTable("EmployeeSchedules", (string)null); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Expense", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Category") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReceiptImageUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ShiftId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("ShiftId"); + + b.HasIndex("CafeId", "BranchId", "CreatedAt"); + + b.ToTable("Expenses"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LowStockWarningPercent") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ParLevel") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("QuantityOnHand") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("ReorderLevel") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("Unit") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("UnitCost") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Ingredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.KitchenStation", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("PrinterIp") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("PrinterPort") + .HasColumnType("integer"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CafeId", "SortOrder"); + + b.ToTable("KitchenStations"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndDate") + .HasColumnType("date"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("ReviewedBy") + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId"); + + b.ToTable("LeaveRequests"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("Icon") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("IconPresetId") + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("IconStyle") + .HasMaxLength(16) + .HasColumnType("character varying(16)"); + + b.Property("ImageUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("KitchenStationId") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("TaxId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("KitchenStationId"); + + b.HasIndex("TaxId"); + + b.ToTable("MenuCategories"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CategoryId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DiscountPercent") + .HasColumnType("numeric"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Model3dUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameAr") + .HasColumnType("text"); + + b.Property("NameEn") + .HasColumnType("text"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("CategoryId"); + + b.ToTable("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItemIngredient", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IngredientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("QuantityPerUnit") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.HasKey("Id"); + + b.HasIndex("IngredientId"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("CafeId", "MenuItemId", "IngredientId") + .IsUnique(); + + b.ToTable("MenuItemIngredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CouponId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeliveryMetaJson") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("DeliveryPlatform") + .HasColumnType("integer"); + + b.Property("DiscountAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("DisplayNumber") + .HasColumnType("integer"); + + b.Property("EmployeeId") + .HasColumnType("text"); + + b.Property("ExternalOrderId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("GuestName") + .HasColumnType("text"); + + b.Property("GuestPhone") + .HasColumnType("text"); + + b.Property("GuestTrackingToken") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("OrderType") + .HasColumnType("integer"); + + b.Property("PlatformCommission") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ReservationId") + .HasColumnType("text"); + + b.Property("SnappfoodOrderId") + .HasColumnType("text"); + + b.Property("Source") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("StatusUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Subtotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("TaxTotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Total") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("CouponId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("GuestTrackingToken"); + + b.HasIndex("ReservationId"); + + b.HasIndex("TableId"); + + b.HasIndex("CafeId", "DisplayNumber") + .IsUnique(); + + b.HasIndex("CafeId", "DeliveryPlatform", "ExternalOrderId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsVoided") + .HasColumnType("boolean"); + + b.Property("MenuItemId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UnitPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("VoidedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VoidedByUserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("integer"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Reference") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.PlatformFeature", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DisplayNameEn") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DisplayNameFa") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("IsEnabledGlobally") + .HasColumnType("boolean"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("ModuleGroup") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("PlatformFeatures"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.PlatformPlanDefinition", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DisplayNameEn") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DisplayNameFa") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("FeaturesJson") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsBillableOnline") + .HasColumnType("boolean"); + + b.Property("LimitsJson") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("MonthlyPriceToman") + .HasPrecision(18) + .HasColumnType("numeric(18,0)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("Tier") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Tier") + .IsUnique(); + + b.ToTable("PlatformPlanDefinitions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.PlatformSetting", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DescriptionFa") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(8000) + .HasColumnType("character varying(8000)"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("PlatformSettings"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.QueueTicket", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerLabel") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedByUserId") + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("integer"); + + b.Property("OrderId") + .HasColumnType("text"); + + b.Property("ServiceDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BranchId"); + + b.HasIndex("OrderId"); + + b.HasIndex("CafeId", "BranchId", "ServiceDate", "Number") + .IsUnique(); + + b.ToTable("QueueTickets"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClosedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ClosedByUserId") + .HasColumnType("text"); + + b.Property("ClosingCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Discrepancy") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ExpectedCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("OpenedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OpenedByUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("OpeningCash") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("ClosedByUserId"); + + b.HasIndex("OpenedByUserId"); + + b.HasIndex("BranchId", "Status"); + + b.ToTable("RegisterShifts", (string)null); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Delta") + .HasPrecision(18, 3) + .HasColumnType("numeric(18,3)"); + + b.Property("ExpenseId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("IngredientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Kind") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("OrderId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("TotalCostToman") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("IngredientId"); + + b.HasIndex("CafeId", "OrderId"); + + b.ToTable("StockMovements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AmountRials") + .HasColumnType("bigint"); + + b.Property("AmountToman") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Authority") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Months") + .HasColumnType("integer"); + + b.Property("PlanTier") + .HasColumnType("integer"); + + b.Property("Provider") + .HasColumnType("integer"); + + b.Property("RefId") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Authority"); + + b.HasIndex("CafeId"); + + b.ToTable("SubscriptionPayments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicket", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AssignedAdminId") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClosedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByEmployeeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Priority") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subject") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedByEmployeeId"); + + b.HasIndex("CafeId", "Status", "UpdatedAt"); + + b.ToTable("SupportTickets"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicketMessage", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Body") + .IsRequired() + .HasMaxLength(8000) + .HasColumnType("character varying(8000)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SenderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("SenderKind") + .HasColumnType("integer"); + + b.Property("TicketId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("TicketId", "CreatedAt"); + + b.ToTable("SupportTicketMessages"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SystemAdmin", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.HasKey("Id"); + + b.HasIndex("Phone") + .IsUnique(); + + b.ToTable("SystemAdmins"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Floor") + .HasColumnType("text"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsCleaning") + .HasColumnType("boolean"); + + b.Property("Number") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("QrCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("SectionId") + .HasColumnType("text"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("VideoUrl") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("QrCode") + .IsUnique(); + + b.HasIndex("SectionId"); + + b.HasIndex("BranchId", "SectionId", "SortOrder"); + + b.ToTable("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("text"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GuestName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GuestPhone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("PartySize") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TableId") + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("time without time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("TableId"); + + b.HasIndex("CafeId", "Date", "Time"); + + b.ToTable("TableReservations"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BranchId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("BranchId", "Name"); + + b.ToTable("TableSections"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CafeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsCompound") + .HasColumnType("boolean"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("IsRequired") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.ToTable("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.WebhookLog", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AttemptCount") + .HasColumnType("integer"); + + b.Property("CafeId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("ExternalOrderId") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("MeeziOrderId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Platform") + .HasColumnType("integer"); + + b.Property("Processed") + .HasColumnType("boolean"); + + b.Property("ProcessedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RawBody") + .IsRequired() + .HasColumnType("text"); + + b.Property("SignatureHeader") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("SignatureValid") + .HasColumnType("boolean"); + + b.Property("Success") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("CafeId"); + + b.HasIndex("Platform", "CreatedAt"); + + b.ToTable("WebhookLogs"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.WebsiteBlogPost", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Author") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("CategoryEn") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CategoryFa") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ContentEn") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContentFa") + .IsRequired() + .HasColumnType("text"); + + b.Property("CoverImage") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExcerptEn") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("ExcerptFa") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("IsPublished") + .HasColumnType("boolean"); + + b.Property("PublishedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("TagsJson") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasDefaultValue("[]"); + + b.Property("TitleEn") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("TitleFa") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("ViewCount") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.HasIndex("IsPublished", "PublishedAt"); + + b.ToTable("WebsiteBlogPosts"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.WebsiteComment", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AuthorEmail") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("AuthorName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(3000) + .HasColumnType("character varying(3000)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsApproved") + .HasColumnType("boolean"); + + b.Property("PostSlug") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.HasKey("Id"); + + b.HasIndex("PostSlug", "IsApproved", "CreatedAt"); + + b.ToTable("WebsiteComments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Attendance", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Attendances") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Branches") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.BranchMenuItemOverride", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("BranchOverrides") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("MenuItem"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeFeatureOverride", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", null) + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Reviews") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReviewPhoto", b => + { + b.HasOne("Meezi.Core.Entities.CafeReview", "Review") + .WithMany("Photos") + .HasForeignKey("ReviewId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Review"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CashTransaction", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Shift", "Shift") + .WithMany("Transactions") + .HasForeignKey("ShiftId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Shift"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Coupons") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Customers") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DailyReport", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.DeliveryCommissionRate", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Staff") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Employees") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Salaries") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.EmployeeSchedule", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Schedules") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Expense", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Shift", "Shift") + .WithMany() + .HasForeignKey("ShiftId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Shift"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Ingredients") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.KitchenStation", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.LeaveRequest", b => + { + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("LeaveRequests") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuCategories") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.KitchenStation", "KitchenStation") + .WithMany("Categories") + .HasForeignKey("KitchenStationId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Tax", "Tax") + .WithMany("MenuCategories") + .HasForeignKey("TaxId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("KitchenStation"); + + b.Navigation("Tax"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("MenuItems") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuCategory", "Category") + .WithMany("MenuItems") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItemIngredient", b => + { + b.HasOne("Meezi.Core.Entities.Ingredient", "Ingredient") + .WithMany("MenuItemRecipes") + .HasForeignKey("IngredientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("RecipeIngredients") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ingredient"); + + b.Navigation("MenuItem"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Orders") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Orders") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Coupon", "Coupon") + .WithMany("Orders") + .HasForeignKey("CouponId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "Employee") + .WithMany("Orders") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.TableReservation", "Reservation") + .WithMany() + .HasForeignKey("ReservationId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany("Orders") + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Coupon"); + + b.Navigation("Customer"); + + b.Navigation("Employee"); + + b.Navigation("Reservation"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.OrderItem", b => + { + b.HasOne("Meezi.Core.Entities.MenuItem", "MenuItem") + .WithMany("OrderItems") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Items") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MenuItem"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Payment", b => + { + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany("Payments") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.QueueTicket", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Order", "Order") + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Employee", "ClosedBy") + .WithMany() + .HasForeignKey("ClosedByUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Employee", "OpenedBy") + .WithMany() + .HasForeignKey("OpenedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("ClosedBy"); + + b.Navigation("OpenedBy"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.StockMovement", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Ingredient", "Ingredient") + .WithMany("Movements") + .HasForeignKey("IngredientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("Ingredient"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SubscriptionPayment", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("SubscriptionPayments") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicket", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Employee", "CreatedByEmployee") + .WithMany() + .HasForeignKey("CreatedByEmployeeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Cafe"); + + b.Navigation("CreatedByEmployee"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicketMessage", b => + { + b.HasOne("Meezi.Core.Entities.SupportTicket", "Ticket") + .WithMany("Messages") + .HasForeignKey("TicketId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ticket"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Tables") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Tables") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.TableSection", "Section") + .WithMany("Tables") + .HasForeignKey("SectionId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("Cafe"); + + b.Navigation("Section"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableReservation", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Meezi.Core.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Meezi.Core.Entities.Table", "Table") + .WithMany() + .HasForeignKey("TableId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Cafe"); + + b.Navigation("Customer"); + + b.Navigation("Table"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.HasOne("Meezi.Core.Entities.Branch", "Branch") + .WithMany("Sections") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Branch"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany("Taxes") + .HasForeignKey("CafeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.WebhookLog", b => + { + b.HasOne("Meezi.Core.Entities.Cafe", "Cafe") + .WithMany() + .HasForeignKey("CafeId"); + + b.Navigation("Cafe"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.WebsiteComment", b => + { + b.HasOne("Meezi.Core.Entities.WebsiteBlogPost", "Post") + .WithMany("Comments") + .HasForeignKey("PostSlug") + .HasPrincipalKey("Slug") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Post"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Branch", b => + { + b.Navigation("Orders"); + + b.Navigation("Sections"); + + b.Navigation("Staff"); + + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Cafe", b => + { + b.Navigation("Branches"); + + b.Navigation("Coupons"); + + b.Navigation("Customers"); + + b.Navigation("Employees"); + + b.Navigation("Ingredients"); + + b.Navigation("MenuCategories"); + + b.Navigation("MenuItems"); + + b.Navigation("Orders"); + + b.Navigation("Reviews"); + + b.Navigation("SubscriptionPayments"); + + b.Navigation("Tables"); + + b.Navigation("Taxes"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.CafeReview", b => + { + b.Navigation("Photos"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Coupon", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Customer", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Employee", b => + { + b.Navigation("Attendances"); + + b.Navigation("LeaveRequests"); + + b.Navigation("Orders"); + + b.Navigation("Salaries"); + + b.Navigation("Schedules"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Ingredient", b => + { + b.Navigation("MenuItemRecipes"); + + b.Navigation("Movements"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.KitchenStation", b => + { + b.Navigation("Categories"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuCategory", b => + { + b.Navigation("MenuItems"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.MenuItem", b => + { + b.Navigation("BranchOverrides"); + + b.Navigation("OrderItems"); + + b.Navigation("RecipeIngredients"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Order", b => + { + b.Navigation("Items"); + + b.Navigation("Payments"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Shift", b => + { + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.SupportTicket", b => + { + b.Navigation("Messages"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Table", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.TableSection", b => + { + b.Navigation("Tables"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.Tax", b => + { + b.Navigation("MenuCategories"); + }); + + modelBuilder.Entity("Meezi.Core.Entities.WebsiteBlogPost", b => + { + b.Navigation("Comments"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Meezi.Infrastructure/Data/PlatformDataSeeder.cs b/src/Meezi.Infrastructure/Data/PlatformDataSeeder.cs new file mode 100644 index 0000000..a48f254 --- /dev/null +++ b/src/Meezi.Infrastructure/Data/PlatformDataSeeder.cs @@ -0,0 +1,320 @@ +using System.Text.Json; +using Meezi.Core.Constants; +using Meezi.Core.Entities; +using Meezi.Core.Enums; +using Meezi.Core.Platform; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Meezi.Infrastructure.Data; + +public static class PlatformDataSeeder +{ + private static readonly JsonSerializerOptions JsonOpts = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + + public static async Task SeedAsync(IServiceProvider services) + { + var env = services.GetRequiredService(); + if (!env.IsDevelopment()) + return; + + var logger = services.GetRequiredService().CreateLogger("PlatformDataSeeder"); + await using var scope = services.CreateAsyncScope(); + var db = scope.ServiceProvider.GetRequiredService(); + + await EnsureCatalogUpgradesAsync(db, logger); + + if (!env.IsDevelopment()) + return; + + await SeedSystemAdminAsync(db, logger); + await SeedPlansAsync(db, logger); + await SeedFeaturesAsync(db, logger); + await SeedSettingsAsync(db, logger); + await EnsureIntegrationSettingsAsync(db, logger); + } + + /// Idempotent plan/feature upgrades for all environments (including production). + public static async Task EnsureCatalogUpgradesAsync(IServiceProvider services) + { + await using var scope = services.CreateAsyncScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var logger = scope.ServiceProvider.GetRequiredService().CreateLogger("PlatformDataSeeder"); + await EnsureCatalogUpgradesAsync(db, logger); + } + + private static async Task EnsureCatalogUpgradesAsync(AppDbContext db, ILogger logger) + { + var featureAdds = new[] + { + ("menu_3d", "منوی سه‌بعدی", "3D menu", "growth"), + ("menu_3d_ai", "تولید ۳D با هوش مصنوعی", "AI 3D menu", "growth"), + ("discover_profile", "پروفایل کشف", "Discover profile", "growth") + }; + + var existingKeys = await db.PlatformFeatures.Select(f => f.Key).ToListAsync(); + var newFeatures = featureAdds + .Where(f => !existingKeys.Contains(f.Item1)) + .Select(f => F(f.Item1, f.Item2, f.Item3, f.Item4)) + .ToList(); + if (newFeatures.Count > 0) + { + db.PlatformFeatures.AddRange(newFeatures); + await db.SaveChangesAsync(); + logger.LogInformation("Platform upgrade: added {Count} features", newFeatures.Count); + } + + var plans = await db.PlatformPlanDefinitions.ToListAsync(); + var changed = 0; + foreach (var plan in plans) + { + if (plan.Tier is PlanTier.Free or PlanTier.Enterprise) + continue; + + var keys = plan.Tier == PlanTier.Business || plan.Tier == PlanTier.Enterprise + ? new[] { "menu_3d", "menu_3d_ai", "discover_profile" } + : new[] { "menu_3d", "discover_profile" }; + var merged = MergeFeaturesJson(plan.FeaturesJson ?? "[]", keys); + if (merged == plan.FeaturesJson) continue; + plan.FeaturesJson = merged; + changed++; + } + + if (changed > 0) + { + await db.SaveChangesAsync(); + logger.LogInformation("Platform upgrade: updated features on {Count} plans", changed); + } + + await EnsureIntegrationSettingsAsync(db, logger); + } + + private static string MergeFeaturesJson(string json, params string[] keys) + { + var list = JsonSerializer.Deserialize>(json, JsonOpts) ?? []; + if (list.Contains("*")) + return json; + + var updated = false; + foreach (var key in keys) + { + if (!list.Contains(key)) + { + list.Add(key); + updated = true; + } + } + + return updated ? JsonSerializer.Serialize(list, JsonOpts) : json; + } + + private static async Task EnsureIntegrationSettingsAsync(AppDbContext db, ILogger logger) + { + var defaults = new[] + { + S("payment.activeGateway", "zarinpal", "payment", "درگاه پیش‌فرض اشتراک"), + S("payment.zarinpal.enabled", "true", "payment", "فعال زرین‌پال"), + S("payment.zarinpal.sandbox", "true", "payment", "حالت تست زرین‌پال"), + S("payment.tara.enabled", "false", "payment", "فعال تارا"), + S("payment.tara.sandbox", "true", "payment", "حالت تست تارا"), + S("payment.snapppay.enabled", "false", "payment", "فعال اسنپ‌پی"), + S("payment.snapppay.sandbox", "true", "payment", "حالت تست اسنپ‌پی"), + S("payment.nextpay.enabled", "false", "payment", "فعال نکست‌پی"), + S("payment.nextpay.sandbox", "true", "payment", "حالت تست نکست‌پی"), + S("payment.vandar.enabled", "false", "payment", "فعال وندار"), + S("payment.vandar.sandbox", "true", "payment", "حالت تست وندار"), + S("integrations.kavenegar.enabled", "true", "integrations", "فعال کاوه‌نگار"), + S("integrations.kavenegar.otpTemplate", "verify", "integrations", "قالب OTP"), + S("integrations.openai.enabled", "false", "integrations", "فعال OpenAI"), + S("integrations.openai.model", "gpt-4o-mini", "integrations", "مدل OpenAI"), + S("integrations.openai.coffeeAdvisor.enabled", "true", "integrations", "مشاور قهوه"), + S("integrations.meshy.enabled", "false", "integrations", "فعال Meshy"), + S("integrations.meshy.menu3d.enabled", "true", "integrations", "ساخت ۳D منو") + }; + + var existing = await db.PlatformSettings.Select(s => s.Key).ToListAsync(); + var missing = defaults.Where(d => !existing.Contains(d.Key)).ToList(); + if (missing.Count == 0) return; + + db.PlatformSettings.AddRange(missing); + await db.SaveChangesAsync(); + logger.LogInformation("Platform seed: added {Count} integration settings", missing.Count); + } + + private static async Task SeedSystemAdminAsync(AppDbContext db, ILogger logger) + { + const string phone = "09120000001"; + if (await db.SystemAdmins.AnyAsync(a => a.Phone == phone)) + return; + + db.SystemAdmins.Add(new SystemAdmin + { + Id = "sysadmin_demo", + Name = "مدیر سامانه", + Phone = phone, + IsActive = true + }); + await db.SaveChangesAsync(); + logger.LogInformation("Platform seed: system admin phone {Phone}", phone); + } + + private static async Task SeedPlansAsync(AppDbContext db, ILogger logger) + { + if (await db.PlatformPlanDefinitions.AnyAsync()) + return; + + var plans = new[] + { + new PlatformPlanDefinition + { + Id = "plan_free", + Tier = PlanTier.Free, + DisplayNameFa = "رایگان", + DisplayNameEn = "Free", + MonthlyPriceToman = 0, + IsBillableOnline = false, + SortOrder = 0, + LimitsJson = JsonSerializer.Serialize(PlanLimitsData.ForTier(PlanTier.Free), JsonOpts), + FeaturesJson = JsonSerializer.Serialize(new[] { "pos", "menu", "tables", "qr_menu" }, JsonOpts) + }, + new PlatformPlanDefinition + { + Id = "plan_pro", + Tier = PlanTier.Pro, + DisplayNameFa = "حرفه‌ای", + DisplayNameEn = "Pro", + MonthlyPriceToman = PlanPricing.MonthlyToman(PlanTier.Pro), + IsBillableOnline = true, + SortOrder = 1, + LimitsJson = JsonSerializer.Serialize(PlanLimitsData.ForTier(PlanTier.Pro), JsonOpts), + FeaturesJson = JsonSerializer.Serialize(new[] + { + "pos", "menu", "tables", "qr_menu", "crm", "coupons", "reports", "kds", "inventory", + "menu_3d", "discover_profile" + }, JsonOpts) + }, + new PlatformPlanDefinition + { + Id = "plan_business", + Tier = PlanTier.Business, + DisplayNameFa = "کسب‌وکار", + DisplayNameEn = "Business", + MonthlyPriceToman = PlanPricing.MonthlyToman(PlanTier.Business), + IsBillableOnline = true, + SortOrder = 2, + LimitsJson = JsonSerializer.Serialize(PlanLimitsData.ForTier(PlanTier.Business), JsonOpts), + FeaturesJson = JsonSerializer.Serialize(new[] + { + "pos", "menu", "tables", "qr_menu", "crm", "coupons", "reports", "kds", "inventory", + "hr", "sms", "reservations", "delivery", "expenses", "branches", + "menu_3d", "menu_3d_ai", "discover_profile" + }, JsonOpts) + }, + new PlatformPlanDefinition + { + Id = "plan_enterprise", + Tier = PlanTier.Enterprise, + DisplayNameFa = "سازمانی", + DisplayNameEn = "Enterprise", + MonthlyPriceToman = 0, + IsBillableOnline = false, + SortOrder = 3, + LimitsJson = JsonSerializer.Serialize(PlanLimitsData.ForTier(PlanTier.Enterprise), JsonOpts), + FeaturesJson = JsonSerializer.Serialize(new[] { "*" }, JsonOpts) + } + }; + + db.PlatformPlanDefinitions.AddRange(plans); + await db.SaveChangesAsync(); + logger.LogInformation("Platform seed: {Count} subscription plans", plans.Length); + } + + private static async Task SeedFeaturesAsync(AppDbContext db, ILogger logger) + { + if (await db.PlatformFeatures.AnyAsync()) + return; + + var features = new[] + { + F("pos", "صندوق", "POS", "core"), + F("menu", "منو", "Menu", "core"), + F("tables", "میزها", "Tables", "core"), + F("qr_menu", "منوی QR", "QR menu", "core"), + F("kds", "آشپزخانه", "KDS", "operations"), + F("crm", "مشتریان", "CRM", "growth"), + F("coupons", "کوپن", "Coupons", "growth"), + F("reports", "گزارش‌ها", "Reports", "analytics"), + F("inventory", "انبار", "Inventory", "operations"), + F("hr", "منابع انسانی", "HR", "operations"), + F("sms", "پیامک", "SMS", "growth"), + F("reservations", "رزرو", "Reservations", "growth"), + F("delivery", "پذیرش آنلاین", "Delivery", "integrations"), + F("expenses", "هزینه‌ها", "Expenses", "analytics"), + F("branches", "چند شعبه", "Branches", "core"), + F("taxes", "مالیات", "Taxes", "compliance"), + F("reviews", "نظرات", "Reviews", "growth"), + F("queue", "صف", "Queue", "operations"), + F("menu_3d", "منوی سه‌بعدی", "3D menu", "growth"), + F("menu_3d_ai", "تولید ۳D با هوش مصنوعی", "AI 3D menu", "growth"), + F("discover_profile", "پروفایل کشف", "Discover profile", "growth") + }; + + db.PlatformFeatures.AddRange(features); + await db.SaveChangesAsync(); + logger.LogInformation("Platform seed: {Count} feature flags", features.Length); + } + + private static PlatformFeature F(string key, string fa, string en, string group) => new() + { + Id = $"feat_{key}", + Key = key, + DisplayNameFa = fa, + DisplayNameEn = en, + ModuleGroup = group, + IsEnabledGlobally = true + }; + + private static async Task SeedSettingsAsync(AppDbContext db, ILogger logger) + { + if (await db.PlatformSettings.AnyAsync()) + return; + + var settings = new[] + { + S("app.name", "میزی", "branding", "نام اپلیکیشن"), + S("app.tagline", "میزت منتظرته", "branding", "شعار"), + S("auth.maxOtpPerHour", "5", "auth", "حداکثر OTP در ساعت"), + S("billing.zarinpalSandbox", "true", "billing", "درگاه تست زرین‌پال"), + S("support.autoCloseDays", "14", "support", "بستن خودکار تیکت پس از روز"), + S("payment.activeGateway", "zarinpal", "payment", "درگاه فعال اشتراک"), + S("payment.zarinpal.enabled", "true", "payment", "فعال زرین‌پال"), + S("payment.zarinpal.sandbox", "true", "payment", "حالت تست زرین‌پال"), + S("payment.nextpay.enabled", "false", "payment", "فعال نکست‌پی"), + S("payment.nextpay.sandbox", "true", "payment", "حالت تست نکست‌پی"), + S("payment.vandar.enabled", "false", "payment", "فعال وندار"), + S("payment.vandar.sandbox", "true", "payment", "حالت تست وندار"), + S("integrations.kavenegar.enabled", "true", "integrations", "فعال کاوه‌نگار"), + S("integrations.kavenegar.otpTemplate", "verify", "integrations", "قالب OTP"), + S("integrations.openai.enabled", "false", "integrations", "فعال OpenAI"), + S("integrations.openai.model", "gpt-4o-mini", "integrations", "مدل OpenAI"), + S("integrations.openai.coffeeAdvisor.enabled", "true", "integrations", "مشاور قهوه"), + S("integrations.meshy.enabled", "false", "integrations", "فعال Meshy"), + S("integrations.meshy.menu3d.enabled", "true", "integrations", "ساخت ۳D منو") + }; + + db.PlatformSettings.AddRange(settings); + await db.SaveChangesAsync(); + logger.LogInformation("Platform seed: {Count} platform settings", settings.Length); + } + + private static PlatformSetting S(string key, string value, string category, string desc) => new() + { + Id = $"cfg_{key.Replace('.', '_')}", + Key = key, + Value = value, + Category = category, + DescriptionFa = desc + }; +} diff --git a/src/Meezi.Infrastructure/Data/TenantContext.cs b/src/Meezi.Infrastructure/Data/TenantContext.cs new file mode 100644 index 0000000..6cb097e --- /dev/null +++ b/src/Meezi.Infrastructure/Data/TenantContext.cs @@ -0,0 +1,16 @@ +using Meezi.Core.Enums; +using Meezi.Core.Interfaces; + +namespace Meezi.Infrastructure.Data; + +public class TenantContext : ITenantContext +{ + public string? UserId { get; set; } + public string? CafeId { get; set; } + public EmployeeRole? Role { get; set; } + public PlanTier? PlanTier { get; set; } + public string? Language { get; set; } + public string? BranchId { get; set; } + public bool IsSystemAdmin { get; set; } + public bool IsAuthenticated => IsSystemAdmin || !string.IsNullOrEmpty(CafeId); +} diff --git a/src/Meezi.Infrastructure/DependencyInjection.cs b/src/Meezi.Infrastructure/DependencyInjection.cs new file mode 100644 index 0000000..5f309bc --- /dev/null +++ b/src/Meezi.Infrastructure/DependencyInjection.cs @@ -0,0 +1,42 @@ +using Meezi.Core.Interfaces; +using Meezi.Infrastructure.Data; +using Meezi.Infrastructure.ExternalServices; +using Meezi.Infrastructure.Services; +using Meezi.Infrastructure.Services.Platform; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Meezi.Infrastructure; + +public static class DependencyInjection +{ + public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration) + { + var connectionString = configuration.GetConnectionString("DefaultConnection") + ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found."); + + services.AddDbContext(options => + options.UseNpgsql(connectionString)); + + services.AddScoped(); + services.AddScoped(sp => sp.GetRequiredService()); + services.AddScoped(); + services.AddScoped(sp => sp.GetRequiredService()); + services.AddScoped(); + services.AddMemoryCache(); + services.AddScoped(); + services.AddScoped(); + + services.AddHttpClient(); + services.AddHttpClient(); + services.AddHttpClient(); + services.AddHttpClient(); + services.AddHttpClient(); + services.AddHttpClient(); + services.AddScoped(); + + return services; + } +} diff --git a/src/Meezi.Infrastructure/Discover/CafeDiscoverProfileSerializer.cs b/src/Meezi.Infrastructure/Discover/CafeDiscoverProfileSerializer.cs new file mode 100644 index 0000000..c786084 --- /dev/null +++ b/src/Meezi.Infrastructure/Discover/CafeDiscoverProfileSerializer.cs @@ -0,0 +1,67 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Meezi.Core.Discover; + +namespace Meezi.Infrastructure.Discover; + +public static class CafeDiscoverProfileSerializer +{ + private static readonly JsonSerializerOptions JsonOptions = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + PropertyNameCaseInsensitive = true + }; + + public static CafeDiscoverProfile Deserialize(string? json) + { + if (string.IsNullOrWhiteSpace(json)) + return new CafeDiscoverProfile(); + + try + { + var profile = JsonSerializer.Deserialize(json, JsonOptions); + return Sanitize(profile ?? new CafeDiscoverProfile()); + } + catch + { + return new CafeDiscoverProfile(); + } + } + + public static string Serialize(CafeDiscoverProfile profile) => + JsonSerializer.Serialize(Sanitize(profile), JsonOptions); + + public static CafeDiscoverProfile Sanitize(CafeDiscoverProfile input) + { + var p = new CafeDiscoverProfile + { + Size = NormalizeSingle(input.Size, CafeDiscoverProfileKeys.Sizes), + Floors = NormalizeSingle(input.Floors, CafeDiscoverProfileKeys.Floors), + NoiseLevel = NormalizeSingle(input.NoiseLevel, CafeDiscoverProfileKeys.NoiseLevels), + PriceTier = NormalizeSingle(input.PriceTier, CafeDiscoverProfileKeys.PriceTiers), + Themes = NormalizeList(input.Themes, CafeDiscoverProfileKeys.Themes), + Vibes = NormalizeList(input.Vibes, CafeDiscoverProfileKeys.Vibes), + Occasions = NormalizeList(input.Occasions, CafeDiscoverProfileKeys.Occasions), + SpaceFeatures = NormalizeList(input.SpaceFeatures, CafeDiscoverProfileKeys.SpaceFeatures) + }; + return p; + } + + private static string? NormalizeSingle(string? value, HashSet allowed) + { + if (string.IsNullOrWhiteSpace(value)) return null; + var id = value.Trim().ToLowerInvariant(); + return allowed.Contains(id) ? id : null; + } + + private static List NormalizeList(IEnumerable? values, HashSet allowed) + { + if (values is null) return []; + return values + .Select(v => v.Trim().ToLowerInvariant()) + .Where(v => !string.IsNullOrEmpty(v) && allowed.Contains(v)) + .Distinct() + .ToList(); + } +} diff --git a/src/Meezi.Infrastructure/Discover/DiscoverBadgeMapping.cs b/src/Meezi.Infrastructure/Discover/DiscoverBadgeMapping.cs new file mode 100644 index 0000000..60b0759 --- /dev/null +++ b/src/Meezi.Infrastructure/Discover/DiscoverBadgeMapping.cs @@ -0,0 +1,33 @@ +using Meezi.Core.Discover; +using Meezi.Core.Entities; +using Meezi.Core.Enums; + +namespace Meezi.Infrastructure.Discover; + +public record CafeBadgeDto(string Key, string Label, string Icon); + +public static class DiscoverBadgeMapping +{ + public static IReadOnlyList ToDtos(Cafe cafe, string lang = "fa") + { + if (cafe.PlanTier != PlanTier.Enterprise) + return []; + + var keys = DiscoverBadgesSerializer.Deserialize(cafe.DiscoverBadgesJson); + var list = new List(); + foreach (var key in keys) + { + var def = CafeBadgeCatalog.Resolve(key); + if (def is null) continue; + var label = lang switch + { + "en" => def.LabelEn, + "ar" => def.LabelAr, + _ => def.LabelFa + }; + list.Add(new CafeBadgeDto(def.Key, label, def.Icon)); + } + + return list; + } +} diff --git a/src/Meezi.Infrastructure/Discover/DiscoverBadgesSerializer.cs b/src/Meezi.Infrastructure/Discover/DiscoverBadgesSerializer.cs new file mode 100644 index 0000000..2d40e4f --- /dev/null +++ b/src/Meezi.Infrastructure/Discover/DiscoverBadgesSerializer.cs @@ -0,0 +1,32 @@ +using System.Text.Json; +using Meezi.Core.Discover; + +namespace Meezi.Infrastructure.Discover; + +public static class DiscoverBadgesSerializer +{ + private static readonly JsonSerializerOptions JsonOptions = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + + public static IReadOnlyList Deserialize(string? json) + { + if (string.IsNullOrWhiteSpace(json)) return []; + try + { + var list = JsonSerializer.Deserialize>(json, JsonOptions); + return CafeBadgeCatalog.NormalizeKeys(list); + } + catch + { + return []; + } + } + + public static string? Serialize(IEnumerable? keys) + { + var normalized = CafeBadgeCatalog.NormalizeKeys(keys); + return normalized.Count == 0 ? null : JsonSerializer.Serialize(normalized, JsonOptions); + } +} diff --git a/src/Meezi.Infrastructure/ExternalServices/.gitkeep b/src/Meezi.Infrastructure/ExternalServices/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/Meezi.Infrastructure/ExternalServices/.gitkeep @@ -0,0 +1 @@ + diff --git a/src/Meezi.Infrastructure/ExternalServices/KavenegarSmsService.cs b/src/Meezi.Infrastructure/ExternalServices/KavenegarSmsService.cs new file mode 100644 index 0000000..5ab65a4 --- /dev/null +++ b/src/Meezi.Infrastructure/ExternalServices/KavenegarSmsService.cs @@ -0,0 +1,112 @@ +using System.Net.Http.Json; +using System.Text.Json.Serialization; +using Meezi.Core.Interfaces; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace Meezi.Infrastructure.ExternalServices; + +public class KavenegarSmsService : ISmsService +{ + private readonly HttpClient _httpClient; + private readonly IConfiguration _configuration; + private readonly IPlatformRuntimeConfig _platform; + private readonly ILogger _logger; + + public KavenegarSmsService( + HttpClient httpClient, + IConfiguration configuration, + IPlatformRuntimeConfig platform, + ILogger logger) + { + _httpClient = httpClient; + _configuration = configuration; + _platform = platform; + _logger = logger; + } + + public async Task SendMessageAsync(string phone, string message, CancellationToken cancellationToken = default) + { + var apiKey = await GetApiKeyAsync(cancellationToken); + if (string.IsNullOrWhiteSpace(apiKey)) + { + _logger.LogInformation( + "Kavenegar API key not configured — SMS to {Phone}: {Message}", + phone, + message); + return; + } + + var url = + $"https://api.kavenegar.com/v1/{apiKey}/sms/send.json" + + $"?receptor={Uri.EscapeDataString(phone)}&message={Uri.EscapeDataString(message)}"; + + var response = await _httpClient.GetAsync(url, cancellationToken); + if (!response.IsSuccessStatusCode) + { + _logger.LogWarning("Kavenegar SMS send failed with status {StatusCode}", response.StatusCode); + throw new InvalidOperationException("SMS delivery failed."); + } + + var body = await response.Content.ReadFromJsonAsync(cancellationToken: cancellationToken); + if (body?.Return?.Status is not 200) + throw new InvalidOperationException("SMS delivery failed."); + } + + public async Task SendOtpAsync(string phone, string otp, CancellationToken cancellationToken = default) + { + var apiKey = await GetApiKeyAsync(cancellationToken); + var template = await GetOtpTemplateAsync(cancellationToken); + + if (string.IsNullOrWhiteSpace(apiKey)) + { + _logger.LogInformation("Kavenegar API key not configured — OTP for {Phone} (dev only, not sent via SMS)", phone); + return; + } + + var url = $"https://api.kavenegar.com/v1/{apiKey}/verify/lookup.json" + + $"?receptor={Uri.EscapeDataString(phone)}&token={otp}&template={Uri.EscapeDataString(template)}"; + + var response = await _httpClient.GetAsync(url, cancellationToken); + if (!response.IsSuccessStatusCode) + { + _logger.LogWarning("Kavenegar OTP send failed with status {StatusCode}", response.StatusCode); + throw new InvalidOperationException("SMS delivery failed."); + } + + var body = await response.Content.ReadFromJsonAsync(cancellationToken: cancellationToken); + if (body?.Return?.Status is not 200) + { + _logger.LogWarning("Kavenegar returned status {Status}", body?.Return?.Status); + throw new InvalidOperationException("SMS delivery failed."); + } + } + + private sealed class KavenegarResponse + { + [JsonPropertyName("return")] + public KavenegarReturn? Return { get; set; } + } + + private sealed class KavenegarReturn + { + [JsonPropertyName("status")] + public int Status { get; set; } + } + + private async Task GetApiKeyAsync(CancellationToken cancellationToken) + { + var fromDb = await _platform.GetAsync("integrations.kavenegar.apiKey", cancellationToken); + if (!string.IsNullOrWhiteSpace(fromDb)) + return fromDb; + return _configuration["Kavenegar:ApiKey"]; + } + + private async Task GetOtpTemplateAsync(CancellationToken cancellationToken) + { + var fromDb = await _platform.GetAsync("integrations.kavenegar.otpTemplate", cancellationToken); + if (!string.IsNullOrWhiteSpace(fromDb)) + return fromDb; + return _configuration["Kavenegar:OtpTemplate"] ?? "verify"; + } +} diff --git a/src/Meezi.Infrastructure/ExternalServices/SnappPayGateway.cs b/src/Meezi.Infrastructure/ExternalServices/SnappPayGateway.cs new file mode 100644 index 0000000..7fad88e --- /dev/null +++ b/src/Meezi.Infrastructure/ExternalServices/SnappPayGateway.cs @@ -0,0 +1,254 @@ +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using Meezi.Core.Interfaces; +using Microsoft.Extensions.Logging; + +namespace Meezi.Infrastructure.ExternalServices; + +/// +/// Snapp Pay online merchant API (BNPL / installment). +/// Docs: https://snapppay.ir — OAuth + payment token + verify/settle. +/// +public class SnappPayGateway : ISnappPayGateway +{ + private const string DefaultProdBase = "https://api.snapppay.ir"; + private const string DefaultSandboxBase = "https://fms-gateway-staging.apps.public.okd4.teh-1.snappcloud.io"; + + private readonly HttpClient _httpClient; + private readonly IPlatformRuntimeConfig _platform; + private readonly ILogger _logger; + + public SnappPayGateway( + HttpClient httpClient, + IPlatformRuntimeConfig platform, + ILogger logger) + { + _httpClient = httpClient; + _platform = platform; + _logger = logger; + } + + public async Task IsEnabledAsync(CancellationToken cancellationToken = default) + { + var enabled = await _platform.GetAsync("payment.snapppay.enabled", cancellationToken); + return enabled is "true"; + } + + public async Task RequestPaymentAsync( + long amountRials, + string transactionId, + string returnUrl, + CancellationToken cancellationToken = default) + { + var creds = await LoadCredentialsAsync(cancellationToken); + if (!creds.IsConfigured) + { + var mockToken = "MOCK-SNAPP-" + transactionId; + var mockUrl = $"{returnUrl}?paymentToken={mockToken}&transactionId={transactionId}&state=OK"; + _logger.LogInformation("SnappPay mock payment {TransactionId} amount {Amount} Rials", transactionId, amountRials); + return new SnappPayInitResult(true, mockToken, mockUrl, null); + } + + var bearer = await GetBearerTokenAsync(creds, cancellationToken); + if (bearer is null) + return new SnappPayInitResult(false, null, null, "Could not obtain Snapp Pay access token."); + + var baseUrl = creds.BaseUrl.TrimEnd('/'); + var payload = new + { + amount = amountRials, + paymentMethodTypeDto = "INSTALLMENT", + returnURL = returnUrl, + transactionId, + externalSourceAmount = 0, + discountAmount = 0, + cartList = new[] + { + new + { + cartId = transactionId, + cartItems = new[] + { + new + { + id = 1, + name = "Meezi subscription", + amount = amountRials, + count = 1, + category = "subscription", + commissionType = 0 + } + } + } + } + }; + + using var req = new HttpRequestMessage(HttpMethod.Post, $"{baseUrl}/api/online/payment/v1/token"); + req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", bearer); + req.Content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json"); + + var response = await _httpClient.SendAsync(req, cancellationToken); + var json = await response.Content.ReadAsStringAsync(cancellationToken); + if (!response.IsSuccessStatusCode) + { + _logger.LogWarning("SnappPay token request failed {Status}: {Body}", response.StatusCode, json); + return new SnappPayInitResult(false, null, null, "Snapp Pay payment token request failed."); + } + + using var doc = JsonDocument.Parse(json); + var root = doc.RootElement; + var paymentToken = FindString(root, "paymentToken", "payment_token", "token"); + var paymentUrl = FindString(root, "paymentPageUrl", "payment_page_url", "redirectUrl", "redirect_url", "url"); + if (string.IsNullOrEmpty(paymentToken) || string.IsNullOrEmpty(paymentUrl)) + return new SnappPayInitResult(false, null, null, "Snapp Pay returned an incomplete response."); + + return new SnappPayInitResult(true, paymentToken, paymentUrl, null); + } + + public async Task VerifyAndSettleAsync( + string paymentToken, + CancellationToken cancellationToken = default) + { + if (paymentToken.StartsWith("MOCK-SNAPP-", StringComparison.Ordinal)) + { + _logger.LogInformation("SnappPay mock verify {Token}", paymentToken); + return new SnappPayVerifyResult(true, paymentToken, null); + } + + var creds = await LoadCredentialsAsync(cancellationToken); + if (!creds.IsConfigured) + return new SnappPayVerifyResult(false, null, "Snapp Pay is not configured."); + + var bearer = await GetBearerTokenAsync(creds, cancellationToken); + if (bearer is null) + return new SnappPayVerifyResult(false, null, "Could not obtain Snapp Pay access token."); + + var baseUrl = creds.BaseUrl.TrimEnd('/'); + var verify = await PostBearerAsync($"{baseUrl}/api/online/payment/v1/verify", bearer, + new { paymentToken }, cancellationToken); + if (!verify.Success) + return new SnappPayVerifyResult(false, null, verify.Error); + + var settle = await PostBearerAsync($"{baseUrl}/api/online/payment/v1/settle", bearer, + new { paymentToken }, cancellationToken); + if (!settle.Success) + return new SnappPayVerifyResult(false, null, settle.Error); + + return new SnappPayVerifyResult(true, paymentToken, null); + } + + private async Task<(bool Success, string? Error)> PostBearerAsync( + string url, + string bearer, + object body, + CancellationToken cancellationToken) + { + using var req = new HttpRequestMessage(HttpMethod.Post, url); + req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", bearer); + req.Content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json"); + var response = await _httpClient.SendAsync(req, cancellationToken); + var json = await response.Content.ReadAsStringAsync(cancellationToken); + if (!response.IsSuccessStatusCode) + { + _logger.LogWarning("SnappPay POST {Url} failed {Status}: {Body}", url, response.StatusCode, json); + return (false, "Snapp Pay API call failed."); + } + + try + { + using var doc = JsonDocument.Parse(json); + if (doc.RootElement.TryGetProperty("successful", out var ok) && ok.ValueKind == JsonValueKind.False) + return (false, FindString(doc.RootElement, "errorData", "message") ?? "Snapp Pay rejected the request."); + } + catch + { + // Some responses are plain success HTTP only + } + + return (true, null); + } + + private async Task GetBearerTokenAsync(SnappPayCredentials creds, CancellationToken cancellationToken) + { + var baseUrl = creds.BaseUrl.TrimEnd('/'); + var basic = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{creds.ClientId}:{creds.ClientSecret}")); + using var req = new HttpRequestMessage(HttpMethod.Post, $"{baseUrl}/api/online/v1/oauth/token"); + req.Headers.Authorization = new AuthenticationHeaderValue("Basic", basic); + req.Content = new FormUrlEncodedContent(new Dictionary + { + ["grant_type"] = "password", + ["scope"] = "online-merchant", + ["username"] = creds.Username, + ["password"] = creds.Password + }); + + var response = await _httpClient.SendAsync(req, cancellationToken); + var json = await response.Content.ReadAsStringAsync(cancellationToken); + if (!response.IsSuccessStatusCode) + { + _logger.LogWarning("SnappPay OAuth failed {Status}: {Body}", response.StatusCode, json); + return null; + } + + var token = JsonSerializer.Deserialize(json); + return token?.AccessToken; + } + + private async Task LoadCredentialsAsync(CancellationToken cancellationToken) + { + var sandbox = await _platform.GetAsync("payment.snapppay.sandbox", cancellationToken); + var isSandbox = sandbox is not "false"; + var baseFromDb = await _platform.GetAsync("payment.snapppay.baseUrl", cancellationToken); + var baseUrl = !string.IsNullOrWhiteSpace(baseFromDb) + ? baseFromDb + : isSandbox ? DefaultSandboxBase : DefaultProdBase; + + return new SnappPayCredentials( + await _platform.GetAsync("payment.snapppay.clientId", cancellationToken) ?? "", + await _platform.GetAsync("payment.snapppay.clientSecret", cancellationToken) ?? "", + await _platform.GetAsync("payment.snapppay.username", cancellationToken) ?? "", + await _platform.GetAsync("payment.snapppay.password", cancellationToken) ?? "", + baseUrl); + } + + private static string? FindString(JsonElement el, params string[] names) + { + foreach (var name in names) + { + if (el.TryGetProperty(name, out var prop) && prop.ValueKind == JsonValueKind.String) + return prop.GetString(); + } + foreach (var prop in el.EnumerateObject()) + { + if (prop.Value.ValueKind == JsonValueKind.Object) + { + var nested = FindString(prop.Value, names); + if (nested is not null) return nested; + } + } + return null; + } + + private sealed record SnappPayCredentials( + string ClientId, + string ClientSecret, + string Username, + string Password, + string BaseUrl) + { + public bool IsConfigured => + !string.IsNullOrWhiteSpace(ClientId) + && !string.IsNullOrWhiteSpace(ClientSecret) + && !string.IsNullOrWhiteSpace(Username) + && !string.IsNullOrWhiteSpace(Password); + } + + private sealed class OAuthTokenResponse + { + [JsonPropertyName("access_token")] + public string? AccessToken { get; set; } + } +} diff --git a/src/Meezi.Infrastructure/ExternalServices/SnappfoodClient.cs b/src/Meezi.Infrastructure/ExternalServices/SnappfoodClient.cs new file mode 100644 index 0000000..8c94e3f --- /dev/null +++ b/src/Meezi.Infrastructure/ExternalServices/SnappfoodClient.cs @@ -0,0 +1,50 @@ +using Meezi.Core.Interfaces; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace Meezi.Infrastructure.ExternalServices; + +public class SnappfoodClient : ISnappfoodClient +{ + private readonly HttpClient _httpClient; + private readonly IConfiguration _configuration; + private readonly ILogger _logger; + + public SnappfoodClient(HttpClient httpClient, IConfiguration configuration, ILogger logger) + { + _httpClient = httpClient; + _configuration = configuration; + _logger = logger; + } + + public Task AcknowledgeOrderAsync(string snappfoodOrderId, CancellationToken cancellationToken = default) => + PostStatusAsync(snappfoodOrderId, "confirmed", cancellationToken); + + public Task NotifyOrderDeliveredAsync(string snappfoodOrderId, CancellationToken cancellationToken = default) => + PostStatusAsync(snappfoodOrderId, "delivered", cancellationToken); + + public Task NotifyOrderStatusAsync( + string snappfoodOrderId, + string status, + CancellationToken cancellationToken = default) => + PostStatusAsync(snappfoodOrderId, status, cancellationToken); + + private async Task PostStatusAsync(string snappfoodOrderId, string status, CancellationToken cancellationToken) + { + var apiKey = _configuration["Snappfood:ApiKey"]; + var baseUrl = _configuration["Snappfood:ApiBaseUrl"]; + if (string.IsNullOrWhiteSpace(apiKey) || string.IsNullOrWhiteSpace(baseUrl)) + { + _logger.LogInformation("Snappfood API not configured — delivered notify for order {OrderId}", snappfoodOrderId); + return; + } + + using var request = new HttpRequestMessage( + HttpMethod.Post, + $"{baseUrl.TrimEnd('/')}/orders/{snappfoodOrderId}/{status}"); + request.Headers.Add("X-Api-Key", apiKey); + var response = await _httpClient.SendAsync(request, cancellationToken); + if (!response.IsSuccessStatusCode) + _logger.LogWarning("Snappfood deliver notify failed {Status} for {OrderId}", response.StatusCode, snappfoodOrderId); + } +} diff --git a/src/Meezi.Infrastructure/ExternalServices/Tap30Client.cs b/src/Meezi.Infrastructure/ExternalServices/Tap30Client.cs new file mode 100644 index 0000000..3aee1e0 --- /dev/null +++ b/src/Meezi.Infrastructure/ExternalServices/Tap30Client.cs @@ -0,0 +1,53 @@ +using System.Net.Http.Json; +using Meezi.Core.Interfaces; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace Meezi.Infrastructure.ExternalServices; + +public class Tap30Client : ITap30Client +{ + private readonly HttpClient _httpClient; + private readonly IConfiguration _configuration; + private readonly ILogger _logger; + + public Tap30Client(HttpClient httpClient, IConfiguration configuration, ILogger logger) + { + _httpClient = httpClient; + _configuration = configuration; + _logger = logger; + } + + public Task AcknowledgeOrderAsync(string tap30OrderId, CancellationToken cancellationToken = default) => + PostStatusAsync(tap30OrderId, "confirmed", cancellationToken); + + public Task NotifyOrderStatusAsync( + string tap30OrderId, + string status, + CancellationToken cancellationToken = default) => + PostStatusAsync(tap30OrderId, status, cancellationToken); + + private async Task PostStatusAsync(string orderId, string status, CancellationToken cancellationToken) + { + var apiKey = _configuration["DeliveryPlatforms:Tap30:ApiKey"] + ?? _configuration["Tap30:ApiKey"]; + var baseUrl = _configuration["DeliveryPlatforms:Tap30:ApiBaseUrl"] + ?? _configuration["Tap30:ApiBaseUrl"]; + + if (string.IsNullOrWhiteSpace(apiKey) || string.IsNullOrWhiteSpace(baseUrl)) + { + _logger.LogInformation("Tap30 API not configured — status {Status} for {OrderId}", status, orderId); + return; + } + + using var request = new HttpRequestMessage( + HttpMethod.Post, + $"{baseUrl.TrimEnd('/')}/orders/{orderId}/status"); + request.Headers.Add("X-Api-Key", apiKey); + request.Content = JsonContent.Create(new { status }); + + var response = await _httpClient.SendAsync(request, cancellationToken); + if (!response.IsSuccessStatusCode) + _logger.LogWarning("Tap30 status callback failed {Status} for {OrderId}", response.StatusCode, orderId); + } +} diff --git a/src/Meezi.Infrastructure/ExternalServices/TaraPaymentGateway.cs b/src/Meezi.Infrastructure/ExternalServices/TaraPaymentGateway.cs new file mode 100644 index 0000000..61e082f --- /dev/null +++ b/src/Meezi.Infrastructure/ExternalServices/TaraPaymentGateway.cs @@ -0,0 +1,280 @@ +using System.Net.Http.Json; +using System.Text.Json; +using Meezi.Core.Interfaces; +using Microsoft.Extensions.Logging; + +namespace Meezi.Infrastructure.ExternalServices; + +/// +/// Tara360 merchant club API (credit / installment). +/// Docs: https://api.tara-club.ir/club/api/v1 — login, trace, request, verify. +/// +public class TaraPaymentGateway : ITaraPaymentGateway +{ + private const string DefaultProdBase = "https://api.tara-club.ir/club/api/v1"; + private const string DefaultSandboxBase = "https://stage.tara-club.ir/club/api/v1"; + + private readonly HttpClient _httpClient; + private readonly IPlatformRuntimeConfig _platform; + private readonly ILogger _logger; + private string? _cachedToken; + private DateTime _tokenExpiresUtc = DateTime.MinValue; + + public TaraPaymentGateway( + HttpClient httpClient, + IPlatformRuntimeConfig platform, + ILogger logger) + { + _httpClient = httpClient; + _platform = platform; + _logger = logger; + } + + public async Task IsEnabledAsync(CancellationToken cancellationToken = default) + { + var enabled = await _platform.GetAsync("payment.tara.enabled", cancellationToken); + return enabled is "true"; + } + + public async Task RequestPaymentAsync( + long amountRials, + string invoiceNumber, + string callbackUrl, + CancellationToken cancellationToken = default) + { + var creds = await LoadCredentialsAsync(cancellationToken); + if (!creds.IsConfigured) + { + var mockTrace = "MOCK-TARA-" + invoiceNumber; + var mockUrl = $"{callbackUrl}?traceNumber={mockTrace}&status=OK"; + _logger.LogInformation("Tara mock payment trace {Trace} amount {Amount} Rials", mockTrace, amountRials); + return new TaraInitResult(true, mockTrace, mockUrl, null); + } + + var token = await LoginAsync(creds, cancellationToken); + if (token is null) + return new TaraInitResult(false, null, null, "Tara login failed."); + + var terminalCode = creds.TerminalCode; + if (string.IsNullOrWhiteSpace(terminalCode)) + { + terminalCode = await ResolveTerminalCodeAsync(creds, token, cancellationToken); + if (string.IsNullOrWhiteSpace(terminalCode)) + return new TaraInitResult(false, null, null, "No Tara terminal available."); + } + + var traceResp = await PostAsync( + creds, + $"purchase/trace/{terminalCode}", + new { }, + token, + cancellationToken); + var traceNumber = FindString(traceResp, "traceNumber", "trace_number", "data"); + if (string.IsNullOrEmpty(traceNumber)) + return new TaraInitResult(false, null, null, "Tara trace creation failed."); + + var amountToman = amountRials / 10; + var purchaseBody = new + { + amount = amountToman, + invoiceNumber, + additionalInfo = "Meezi subscription", + invoiceItems = new[] + { + new + { + name = "Meezi subscription", + code = "MEEZI-SUB", + count = 1, + unit = 1, + price = amountToman, + group = "GENERAL", + groupTitle = "عمومی" + } + } + }; + + var requestResp = await PostAsync( + creds, + $"purchase/request/{traceNumber}", + purchaseBody, + token, + cancellationToken); + + var paymentUrl = FindString(requestResp, "paymentUrl", "payment_url", "redirectUrl", "url"); + if (string.IsNullOrEmpty(paymentUrl)) + paymentUrl = $"{callbackUrl}?traceNumber={traceNumber}&status=OK"; + + return new TaraInitResult(true, traceNumber, paymentUrl, null); + } + + public async Task VerifyPaymentAsync( + string traceNumber, + CancellationToken cancellationToken = default) + { + if (traceNumber.StartsWith("MOCK-TARA-", StringComparison.Ordinal)) + { + _logger.LogInformation("Tara mock verify {Trace}", traceNumber); + return new TaraVerifyResult(true, traceNumber, null); + } + + var creds = await LoadCredentialsAsync(cancellationToken); + if (!creds.IsConfigured) + return new TaraVerifyResult(false, null, "Tara is not configured."); + + var token = await LoginAsync(creds, cancellationToken); + if (token is null) + return new TaraVerifyResult(false, null, "Tara login failed."); + + var verifyResp = await PostAsync( + creds, + $"purchase/verify/{traceNumber}", + new { }, + token, + cancellationToken); + + var success = FindBool(verifyResp, "success", "isSuccess") ?? true; + if (!success) + return new TaraVerifyResult(false, null, "Tara verify failed."); + + var refId = FindString(verifyResp, "referenceNumber", "refId", "id") ?? traceNumber; + return new TaraVerifyResult(true, refId, null); + } + + private async Task LoginAsync(TaraCredentials creds, CancellationToken cancellationToken) + { + if (_cachedToken is not null && DateTime.UtcNow < _tokenExpiresUtc) + return _cachedToken; + + var resp = await PostAsync( + creds, + "merchant/login", + new { username = creds.Username, password = creds.Password }, + null, + cancellationToken); + + var token = FindString(resp, "token", "accessToken", "access_token"); + if (string.IsNullOrEmpty(token)) + return null; + + _cachedToken = token; + _tokenExpiresUtc = DateTime.UtcNow.AddMinutes(25); + return token; + } + + private async Task ResolveTerminalCodeAsync( + TaraCredentials creds, + string token, + CancellationToken cancellationToken) + { + var resp = await PostAsync( + creds, + $"merchant/access/code/{creds.BranchCode}", + new { }, + token, + cancellationToken); + + if (resp.ValueKind == JsonValueKind.Array && resp.GetArrayLength() > 0) + { + var first = resp[0]; + return FindString(first, "terminalCode", "code"); + } + + return FindString(resp, "terminalCode", "code"); + } + + private async Task PostAsync( + TaraCredentials creds, + string path, + object body, + string? bearer, + CancellationToken cancellationToken) + { + using var req = new HttpRequestMessage(HttpMethod.Post, $"{creds.BaseUrl.TrimEnd('/')}/{path.TrimStart('/')}"); + if (!string.IsNullOrEmpty(bearer)) + req.Headers.TryAddWithoutValidation("Authorization", $"Bearer {bearer}"); + req.Content = JsonContent.Create(body); + var response = await _httpClient.SendAsync(req, cancellationToken); + var json = await response.Content.ReadAsStringAsync(cancellationToken); + if (!response.IsSuccessStatusCode) + { + _logger.LogWarning("Tara POST {Path} failed {Status}: {Body}", path, response.StatusCode, json); + return default!; + } + + return JsonSerializer.Deserialize(json)!; + } + + private async Task LoadCredentialsAsync(CancellationToken cancellationToken) + { + var sandbox = await _platform.GetAsync("payment.tara.sandbox", cancellationToken); + var isSandbox = sandbox is not "false"; + var baseFromDb = await _platform.GetAsync("payment.tara.baseUrl", cancellationToken); + var baseUrl = !string.IsNullOrWhiteSpace(baseFromDb) + ? baseFromDb + : isSandbox ? DefaultSandboxBase : DefaultProdBase; + + return new TaraCredentials( + await _platform.GetAsync("payment.tara.username", cancellationToken) ?? "", + await _platform.GetAsync("payment.tara.password", cancellationToken) ?? "", + await _platform.GetAsync("payment.tara.branchCode", cancellationToken) ?? "", + await _platform.GetAsync("payment.tara.terminalCode", cancellationToken), + baseUrl); + } + + private static string? FindString(JsonElement el, params string[] names) + { + if (el.ValueKind == JsonValueKind.String) + return el.GetString(); + + foreach (var name in names) + { + if (el.TryGetProperty(name, out var prop)) + { + if (prop.ValueKind == JsonValueKind.String) + return prop.GetString(); + if (prop.ValueKind == JsonValueKind.Number) + return prop.GetRawText(); + } + } + + foreach (var prop in el.EnumerateObject()) + { + if (prop.Value.ValueKind is JsonValueKind.Object or JsonValueKind.Array) + { + if (prop.Value.ValueKind == JsonValueKind.Array && prop.Value.GetArrayLength() > 0) + { + var nested = FindString(prop.Value[0], names); + if (nested is not null) return nested; + } + var inner = FindString(prop.Value, names); + if (inner is not null) return inner; + } + } + + return null; + } + + private static bool? FindBool(JsonElement el, params string[] names) + { + foreach (var name in names) + { + if (el.TryGetProperty(name, out var prop) && prop.ValueKind is JsonValueKind.True or JsonValueKind.False) + return prop.GetBoolean(); + } + return null; + } + + private sealed record TaraCredentials( + string Username, + string Password, + string BranchCode, + string? TerminalCode, + string BaseUrl) + { + public bool IsConfigured => + !string.IsNullOrWhiteSpace(Username) + && !string.IsNullOrWhiteSpace(Password) + && !string.IsNullOrWhiteSpace(BranchCode); + } +} diff --git a/src/Meezi.Infrastructure/ExternalServices/TarazTaxService.cs b/src/Meezi.Infrastructure/ExternalServices/TarazTaxService.cs new file mode 100644 index 0000000..f77a90b --- /dev/null +++ b/src/Meezi.Infrastructure/ExternalServices/TarazTaxService.cs @@ -0,0 +1,39 @@ +using Meezi.Core.Interfaces; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace Meezi.Infrastructure.ExternalServices; + +public class TarazTaxService : ITarazTaxService +{ + private readonly IConfiguration _configuration; + private readonly ILogger _logger; + + public TarazTaxService(IConfiguration configuration, ILogger logger) + { + _configuration = configuration; + _logger = logger; + } + + public Task SubmitDailyInvoicesAsync( + string cafeId, + DateTime dateUtc, + CancellationToken cancellationToken = default) + { + var username = _configuration["Taraz:Username"]; + if (string.IsNullOrWhiteSpace(username)) + { + _logger.LogInformation( + "Taraz not configured — skip submit for cafe {CafeId} date {Date}", + cafeId, + dateUtc.Date); + return Task.FromResult(new TarazSubmitResult( + true, + "MOCK-TARAZ", + "Taraz API not configured; submission logged only.")); + } + + _logger.LogInformation("Taraz submit queued for cafe {CafeId} on {Date}", cafeId, dateUtc.Date); + return Task.FromResult(new TarazSubmitResult(true, null, "Submission queued.")); + } +} diff --git a/src/Meezi.Infrastructure/ExternalServices/ZarinPalGateway.cs b/src/Meezi.Infrastructure/ExternalServices/ZarinPalGateway.cs new file mode 100644 index 0000000..52a0e5e --- /dev/null +++ b/src/Meezi.Infrastructure/ExternalServices/ZarinPalGateway.cs @@ -0,0 +1,151 @@ +using System.Net.Http.Json; +using System.Text.Json.Serialization; +using Meezi.Core.Interfaces; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace Meezi.Infrastructure.ExternalServices; + +public class ZarinPalGateway : IZarinPalGateway +{ + public async Task IsEnabledAsync(CancellationToken cancellationToken = default) + { + var enabled = await _platform.GetAsync("payment.zarinpal.enabled", cancellationToken); + return enabled is not "false"; + } + + private readonly HttpClient _httpClient; + private readonly IConfiguration _configuration; + private readonly IPlatformRuntimeConfig _platform; + private readonly ILogger _logger; + + public ZarinPalGateway( + HttpClient httpClient, + IConfiguration configuration, + IPlatformRuntimeConfig platform, + ILogger logger) + { + _httpClient = httpClient; + _configuration = configuration; + _platform = platform; + _logger = logger; + } + + public async Task RequestPaymentAsync( + long amountRials, + string description, + string callbackUrl, + CancellationToken cancellationToken = default) + { + var merchantId = await GetMerchantIdAsync(cancellationToken); + if (string.IsNullOrWhiteSpace(merchantId)) + { + var mockAuthority = Guid.NewGuid().ToString("N")[..16]; + var mockUrl = $"{callbackUrl}?Authority={mockAuthority}&Status=OK"; + _logger.LogInformation("ZarinPal mock payment {Authority} amount {Amount} Rials", mockAuthority, amountRials); + return new ZarinPalRequestResult(true, mockAuthority, mockUrl, null); + } + + var sandbox = await IsSandboxAsync(cancellationToken); + var baseUrl = sandbox + ? "https://sandbox.zarinpal.com/pg/v4/payment" + : "https://api.zarinpal.com/pg/v4/payment"; + + var payload = new + { + merchant_id = merchantId, + amount = amountRials, + description, + callback_url = callbackUrl + }; + + var response = await _httpClient.PostAsJsonAsync($"{baseUrl}/request.json", payload, cancellationToken); + var body = await response.Content.ReadFromJsonAsync(cancellationToken: cancellationToken); + + if (body?.Data?.Code is 100 && !string.IsNullOrEmpty(body.Data.Authority)) + { + var startUrl = sandbox + ? $"https://sandbox.zarinpal.com/pg/StartPay/{body.Data.Authority}" + : $"https://www.zarinpal.com/pg/StartPay/{body.Data.Authority}"; + return new ZarinPalRequestResult(true, body.Data.Authority, startUrl, null); + } + + return new ZarinPalRequestResult(false, null, null, body?.Errors?.FirstOrDefault()?.Message ?? "ZarinPal request failed."); + } + + public async Task VerifyPaymentAsync( + string authority, + long amountRials, + CancellationToken cancellationToken = default) + { + var merchantId = await GetMerchantIdAsync(cancellationToken); + if (string.IsNullOrWhiteSpace(merchantId)) + { + _logger.LogInformation("ZarinPal mock verify authority {Authority}", authority); + return new ZarinPalVerifyResult(true, "MOCK-" + authority[..8], null); + } + + var sandbox = await IsSandboxAsync(cancellationToken); + var baseUrl = sandbox + ? "https://sandbox.zarinpal.com/pg/v4/payment" + : "https://api.zarinpal.com/pg/v4/payment"; + + var payload = new + { + merchant_id = merchantId, + amount = amountRials, + authority + }; + + var response = await _httpClient.PostAsJsonAsync($"{baseUrl}/verify.json", payload, cancellationToken); + var body = await response.Content.ReadFromJsonAsync(cancellationToken: cancellationToken); + + if (body?.Data?.Code is 100 or 101) + return new ZarinPalVerifyResult(true, body.Data.RefId?.ToString(), null); + + return new ZarinPalVerifyResult(false, null, body?.Errors?.FirstOrDefault()?.Message ?? "ZarinPal verify failed."); + } + + private sealed class ZarinPalDataResponse + { + [JsonPropertyName("data")] + public ZarinPalData? Data { get; set; } + + [JsonPropertyName("errors")] + public List? Errors { get; set; } + } + + private sealed class ZarinPalData + { + [JsonPropertyName("code")] + public int Code { get; set; } + + [JsonPropertyName("authority")] + public string? Authority { get; set; } + + [JsonPropertyName("ref_id")] + public long? RefId { get; set; } + } + + private sealed class ZarinPalError + { + [JsonPropertyName("message")] + public string? Message { get; set; } + } + + private async Task GetMerchantIdAsync(CancellationToken cancellationToken) + { + var fromDb = await _platform.GetAsync("payment.zarinpal.merchantId", cancellationToken); + if (!string.IsNullOrWhiteSpace(fromDb)) + return fromDb; + return _configuration["ZarinPal:MerchantId"]; + } + + private async Task IsSandboxAsync(CancellationToken cancellationToken) + { + var fromDb = await _platform.GetAsync("payment.zarinpal.sandbox", cancellationToken); + if (!string.IsNullOrWhiteSpace(fromDb)) + return fromDb is not "false"; + return _configuration.GetValue("ZarinPal:Sandbox", true); + } +} diff --git a/src/Meezi.Infrastructure/Meezi.Infrastructure.csproj b/src/Meezi.Infrastructure/Meezi.Infrastructure.csproj new file mode 100644 index 0000000..98893f2 --- /dev/null +++ b/src/Meezi.Infrastructure/Meezi.Infrastructure.csproj @@ -0,0 +1,20 @@ + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + diff --git a/src/Meezi.Infrastructure/Models/SupportDtos.cs b/src/Meezi.Infrastructure/Models/SupportDtos.cs new file mode 100644 index 0000000..ca1edd6 --- /dev/null +++ b/src/Meezi.Infrastructure/Models/SupportDtos.cs @@ -0,0 +1,38 @@ +using Meezi.Core.Enums; + +namespace Meezi.Infrastructure.Models; + +public record SupportTicketDto( + string Id, + string CafeId, + string CafeName, + string Subject, + SupportTicketStatus Status, + SupportTicketPriority Priority, + string CreatedByEmployeeId, + string? CreatedByName, + string? AssignedAdminId, + DateTime CreatedAt, + DateTime UpdatedAt, + int MessageCount); + +public record SupportTicketDetailDto( + SupportTicketDto Ticket, + IReadOnlyList Messages); + +public record SupportTicketMessageDto( + string Id, + TicketMessageSenderKind SenderKind, + string SenderId, + string? SenderName, + string Body, + DateTime CreatedAt); + +public record CreateSupportTicketRequest(string Subject, string Body, SupportTicketPriority? Priority); + +public record ReplySupportTicketRequest(string Body); + +public record UpdateSupportTicketRequest( + SupportTicketStatus? Status, + SupportTicketPriority? Priority, + string? AssignedAdminId); diff --git a/src/Meezi.Infrastructure/Repositories/.gitkeep b/src/Meezi.Infrastructure/Repositories/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/Meezi.Infrastructure/Repositories/.gitkeep @@ -0,0 +1 @@ + diff --git a/src/Meezi.Infrastructure/Services/Platform/IPlatformCatalogService.cs b/src/Meezi.Infrastructure/Services/Platform/IPlatformCatalogService.cs new file mode 100644 index 0000000..7f337a9 --- /dev/null +++ b/src/Meezi.Infrastructure/Services/Platform/IPlatformCatalogService.cs @@ -0,0 +1,57 @@ +using Meezi.Core.Enums; +using Meezi.Core.Platform; + +namespace Meezi.Infrastructure.Services.Platform; + +public interface IPlatformCatalogService +{ + Task> GetPlansAsync(CancellationToken cancellationToken = default); + Task GetPlanAsync(PlanTier tier, CancellationToken cancellationToken = default); + Task GetLimitsAsync(PlanTier tier, CancellationToken cancellationToken = default); + Task GetMonthlyPriceTomanAsync(PlanTier tier, CancellationToken cancellationToken = default); + Task IsBillableOnlineAsync(PlanTier tier, CancellationToken cancellationToken = default); + + Task> GetSettingsAsync(CancellationToken cancellationToken = default); + Task GetSettingAsync(string key, CancellationToken cancellationToken = default); + + Task> GetFeaturesAsync(CancellationToken cancellationToken = default); + Task> GetEffectiveFeaturesForCafeAsync( + string cafeId, + PlanTier planTier, + CancellationToken cancellationToken = default); + Task IsFeatureEnabledForCafeAsync( + string cafeId, + PlanTier planTier, + string featureKey, + CancellationToken cancellationToken = default); + + void InvalidateCache(); +} + +public record PlanDefinitionDto( + PlanTier Tier, + string DisplayNameFa, + string? DisplayNameEn, + decimal MonthlyPriceToman, + bool IsBillableOnline, + bool IsActive, + int SortOrder, + PlanLimitsData Limits, + IReadOnlyList FeatureKeys); + +public record PlatformSettingDto( + string Id, + string Key, + string Value, + string Category, + string? DescriptionFa, + DateTime UpdatedAt); + +public record PlatformFeatureDto( + string Id, + string Key, + string DisplayNameFa, + string? DisplayNameEn, + string ModuleGroup, + bool IsEnabledGlobally, + DateTime UpdatedAt); diff --git a/src/Meezi.Infrastructure/Services/Platform/PlatformCatalogService.cs b/src/Meezi.Infrastructure/Services/Platform/PlatformCatalogService.cs new file mode 100644 index 0000000..054cbd5 --- /dev/null +++ b/src/Meezi.Infrastructure/Services/Platform/PlatformCatalogService.cs @@ -0,0 +1,213 @@ +using System.Text.Json; +using Meezi.Core.Constants; +using Meezi.Core.Entities; +using Meezi.Core.Enums; +using Meezi.Core.Platform; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Caching.Memory; + +namespace Meezi.Infrastructure.Services.Platform; + +public class PlatformCatalogService : IPlatformCatalogService +{ + private static readonly JsonSerializerOptions JsonOpts = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + private const string PlansCacheKey = "platform:plans"; + private const string FeaturesCacheKey = "platform:features"; + private const string SettingsCacheKey = "platform:settings"; + + private readonly AppDbContext _db; + private readonly IMemoryCache _cache; + + public PlatformCatalogService(AppDbContext db, IMemoryCache memoryCache) + { + _db = db; + _cache = memoryCache; + } + + public void InvalidateCache() + { + _cache.Remove(PlansCacheKey); + _cache.Remove(FeaturesCacheKey); + _cache.Remove(SettingsCacheKey); + } + + public async Task> GetPlansAsync(CancellationToken cancellationToken = default) + { + var plans = await LoadPlansAsync(cancellationToken); + return plans; + } + + public async Task GetPlanAsync(PlanTier tier, CancellationToken cancellationToken = default) + { + var plans = await LoadPlansAsync(cancellationToken); + return plans.FirstOrDefault(p => p.Tier == tier); + } + + public async Task GetLimitsAsync(PlanTier tier, CancellationToken cancellationToken = default) + { + var plan = await GetPlanAsync(tier, cancellationToken); + return plan?.Limits ?? PlanLimitsData.ForTier(tier); + } + + public async Task GetMonthlyPriceTomanAsync(PlanTier tier, CancellationToken cancellationToken = default) + { + var plan = await GetPlanAsync(tier, cancellationToken); + return plan?.MonthlyPriceToman ?? PlanPricing.MonthlyToman(tier); + } + + public async Task IsBillableOnlineAsync(PlanTier tier, CancellationToken cancellationToken = default) + { + var plan = await GetPlanAsync(tier, cancellationToken); + return plan?.IsBillableOnline ?? PlanPricing.IsBillableOnline(tier); + } + + public async Task> GetSettingsAsync(CancellationToken cancellationToken = default) + { + return await _cache.GetOrCreateAsync(SettingsCacheKey, async entry => + { + entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(2); + return await _db.PlatformSettings + .AsNoTracking() + .OrderBy(s => s.Category) + .ThenBy(s => s.Key) + .Select(s => new PlatformSettingDto(s.Id, s.Key, s.Value, s.Category, s.DescriptionFa, s.UpdatedAt)) + .ToListAsync(cancellationToken); + }) ?? []; + } + + public async Task GetSettingAsync(string key, CancellationToken cancellationToken = default) + { + var settings = await GetSettingsAsync(cancellationToken); + return settings.FirstOrDefault(s => s.Key == key)?.Value; + } + + public async Task> GetFeaturesAsync(CancellationToken cancellationToken = default) + { + return await _cache.GetOrCreateAsync(FeaturesCacheKey, async entry => + { + entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(2); + return await _db.PlatformFeatures + .AsNoTracking() + .OrderBy(f => f.ModuleGroup) + .ThenBy(f => f.Key) + .Select(f => new PlatformFeatureDto( + f.Id, f.Key, f.DisplayNameFa, f.DisplayNameEn, f.ModuleGroup, f.IsEnabledGlobally, f.UpdatedAt)) + .ToListAsync(cancellationToken); + }) ?? []; + } + + public async Task> GetEffectiveFeaturesForCafeAsync( + string cafeId, + PlanTier planTier, + CancellationToken cancellationToken = default) + { + var features = await GetFeaturesAsync(cancellationToken); + var plan = await GetPlanAsync(planTier, cancellationToken); + var planKeys = plan?.FeatureKeys ?? []; + var allPlan = planKeys.Contains("*"); + + var overrides = await _db.CafeFeatureOverrides + .AsNoTracking() + .Where(o => o.CafeId == cafeId) + .ToDictionaryAsync(o => o.FeatureKey, o => o.IsEnabled, cancellationToken); + + var result = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var feature in features) + { + if (overrides.TryGetValue(feature.Key, out var overrideEnabled)) + { + result[feature.Key] = overrideEnabled; + continue; + } + + if (!feature.IsEnabledGlobally) + { + result[feature.Key] = false; + continue; + } + + result[feature.Key] = allPlan || planKeys.Contains(feature.Key, StringComparer.OrdinalIgnoreCase); + } + + return result; + } + + public async Task IsFeatureEnabledForCafeAsync( + string cafeId, + PlanTier planTier, + string featureKey, + CancellationToken cancellationToken = default) + { + var map = await GetEffectiveFeaturesForCafeAsync(cafeId, planTier, cancellationToken); + return map.TryGetValue(featureKey, out var enabled) && enabled; + } + + private async Task> LoadPlansAsync(CancellationToken cancellationToken) + { + return await _cache.GetOrCreateAsync(PlansCacheKey, async entry => + { + entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(2); + var rows = await _db.PlatformPlanDefinitions + .AsNoTracking() + .Where(p => p.IsActive) + .OrderBy(p => p.SortOrder) + .ToListAsync(cancellationToken); + + if (rows.Count == 0) + return DefaultPlansFromConstants(); + + return rows.Select(MapPlan).ToList(); + }) ?? DefaultPlansFromConstants(); + } + + private static IReadOnlyList DefaultPlansFromConstants() => + Enum.GetValues().Select(tier => new PlanDefinitionDto( + tier, + tier.ToString(), + tier.ToString(), + PlanPricing.MonthlyToman(tier), + PlanPricing.IsBillableOnline(tier), + true, + (int)tier, + PlanLimitsData.ForTier(tier), + [])).ToList(); + + private static PlanDefinitionDto MapPlan(PlatformPlanDefinition row) + { + PlanLimitsData limits; + try + { + limits = JsonSerializer.Deserialize(row.LimitsJson, JsonOpts) + ?? PlanLimitsData.ForTier(row.Tier); + } + catch + { + limits = PlanLimitsData.ForTier(row.Tier); + } + + IReadOnlyList featureKeys = []; + if (!string.IsNullOrWhiteSpace(row.FeaturesJson)) + { + try + { + featureKeys = JsonSerializer.Deserialize(row.FeaturesJson, JsonOpts) ?? []; + } + catch + { + featureKeys = []; + } + } + + return new PlanDefinitionDto( + row.Tier, + row.DisplayNameFa, + row.DisplayNameEn, + row.MonthlyPriceToman, + row.IsBillableOnline, + row.IsActive, + row.SortOrder, + limits, + featureKeys); + } +} diff --git a/src/Meezi.Infrastructure/Services/PlatformRuntimeConfig.cs b/src/Meezi.Infrastructure/Services/PlatformRuntimeConfig.cs new file mode 100644 index 0000000..32d2818 --- /dev/null +++ b/src/Meezi.Infrastructure/Services/PlatformRuntimeConfig.cs @@ -0,0 +1,57 @@ +using Meezi.Core.Interfaces; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Configuration; + +namespace Meezi.Infrastructure.Services; + +public class PlatformRuntimeConfig : IPlatformRuntimeConfig +{ + private const string CacheKey = "platform:settings:all"; + private readonly AppDbContext _db; + private readonly IConfiguration _configuration; + private readonly IMemoryCache _cache; + + public PlatformRuntimeConfig( + AppDbContext db, + IConfiguration configuration, + IMemoryCache cache) + { + _db = db; + _configuration = configuration; + _cache = cache; + } + + public void InvalidateCache() => _cache.Remove(CacheKey); + + public async Task GetAsync(string key, CancellationToken cancellationToken = default) + { + var map = await LoadMapAsync(cancellationToken); + if (map.TryGetValue(key, out var v) && !string.IsNullOrWhiteSpace(v)) + return v; + return _configuration[key.Replace('.', ':')]; + } + + public async Task> GetByPrefixAsync( + string prefix, + CancellationToken cancellationToken = default) + { + var map = await LoadMapAsync(cancellationToken); + return map + .Where(kv => kv.Key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + .ToDictionary(kv => kv.Key, kv => kv.Value, StringComparer.OrdinalIgnoreCase); + } + + private async Task> LoadMapAsync(CancellationToken cancellationToken) + { + return await _cache.GetOrCreateAsync(CacheKey, async entry => + { + entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(2); + var rows = await _db.PlatformSettings.AsNoTracking() + .Select(s => new { s.Key, s.Value }) + .ToListAsync(cancellationToken); + return rows.ToDictionary(r => r.Key, r => r.Value, StringComparer.OrdinalIgnoreCase); + }) ?? new Dictionary(StringComparer.OrdinalIgnoreCase); + } +} diff --git a/src/Meezi.Infrastructure/Services/SupportTicketService.cs b/src/Meezi.Infrastructure/Services/SupportTicketService.cs new file mode 100644 index 0000000..6dffa6b --- /dev/null +++ b/src/Meezi.Infrastructure/Services/SupportTicketService.cs @@ -0,0 +1,277 @@ +using Meezi.Infrastructure.Models; +using Meezi.Core.Entities; +using Meezi.Core.Enums; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; + +namespace Meezi.Infrastructure.Services; + +public interface ISupportTicketService +{ + Task> ListForCafeAsync(string cafeId, CancellationToken cancellationToken = default); + Task GetForCafeAsync(string cafeId, string ticketId, CancellationToken cancellationToken = default); + Task CreateForCafeAsync( + string cafeId, + string employeeId, + CreateSupportTicketRequest request, + CancellationToken cancellationToken = default); + Task ReplyAsMerchantAsync( + string cafeId, + string ticketId, + string employeeId, + ReplySupportTicketRequest request, + CancellationToken cancellationToken = default); + + Task> ListAllAsync( + SupportTicketStatus? status, + CancellationToken cancellationToken = default); + Task GetAdminAsync(string ticketId, CancellationToken cancellationToken = default); + Task ReplyAsAdminAsync( + string ticketId, + string adminId, + ReplySupportTicketRequest request, + CancellationToken cancellationToken = default); + Task UpdateAdminAsync( + string ticketId, + UpdateSupportTicketRequest request, + CancellationToken cancellationToken = default); +} + +public class SupportTicketService : ISupportTicketService +{ + private readonly AppDbContext _db; + + public SupportTicketService(AppDbContext db) + { + _db = db; + } + + public async Task> ListForCafeAsync( + string cafeId, + CancellationToken cancellationToken = default) + { + return await QueryTickets() + .Where(t => t.CafeId == cafeId) + .OrderByDescending(t => t.UpdatedAt) + .ToListAsync(cancellationToken); + } + + public async Task GetForCafeAsync( + string cafeId, + string ticketId, + CancellationToken cancellationToken = default) + { + var ticket = await _db.SupportTickets + .AsNoTracking() + .Include(t => t.Cafe) + .Include(t => t.CreatedByEmployee) + .FirstOrDefaultAsync(t => t.Id == ticketId && t.CafeId == cafeId, cancellationToken); + if (ticket is null) return null; + + var messages = await LoadMessagesAsync(ticketId, cancellationToken); + return new SupportTicketDetailDto(MapTicket(ticket), messages); + } + + public async Task CreateForCafeAsync( + string cafeId, + string employeeId, + CreateSupportTicketRequest request, + CancellationToken cancellationToken = default) + { + var ticket = new SupportTicket + { + CafeId = cafeId, + Subject = request.Subject.Trim(), + Priority = request.Priority ?? SupportTicketPriority.Normal, + CreatedByEmployeeId = employeeId, + Status = SupportTicketStatus.Open + }; + _db.SupportTickets.Add(ticket); + await _db.SaveChangesAsync(cancellationToken); + + await AddMessageAsync(ticket.Id, TicketMessageSenderKind.Merchant, employeeId, request.Body, cancellationToken); + ticket.UpdatedAt = DateTime.UtcNow; + await _db.SaveChangesAsync(cancellationToken); + return await GetForCafeAsync(cafeId, ticket.Id, cancellationToken); + } + + public async Task ReplyAsMerchantAsync( + string cafeId, + string ticketId, + string employeeId, + ReplySupportTicketRequest request, + CancellationToken cancellationToken = default) + { + var ticket = await _db.SupportTickets + .FirstOrDefaultAsync(t => t.Id == ticketId && t.CafeId == cafeId, cancellationToken); + if (ticket is null) return null; + if (ticket.Status is SupportTicketStatus.Closed or SupportTicketStatus.Resolved) + return null; + + await AddMessageAsync(ticketId, TicketMessageSenderKind.Merchant, employeeId, request.Body, cancellationToken); + ticket.Status = SupportTicketStatus.Open; + ticket.UpdatedAt = DateTime.UtcNow; + await _db.SaveChangesAsync(cancellationToken); + return await GetForCafeAsync(cafeId, ticketId, cancellationToken); + } + + public async Task> ListAllAsync( + SupportTicketStatus? status, + CancellationToken cancellationToken = default) + { + var q = QueryTickets(); + if (status.HasValue) + q = q.Where(t => t.Status == status.Value); + + return await q.OrderByDescending(t => t.UpdatedAt).ToListAsync(cancellationToken); + } + + public async Task GetAdminAsync( + string ticketId, + CancellationToken cancellationToken = default) + { + var ticket = await _db.SupportTickets + .AsNoTracking() + .Include(t => t.Cafe) + .Include(t => t.CreatedByEmployee) + .FirstOrDefaultAsync(t => t.Id == ticketId, cancellationToken); + if (ticket is null) return null; + + var messages = await LoadMessagesAsync(ticketId, cancellationToken); + return new SupportTicketDetailDto(MapTicket(ticket), messages); + } + + public async Task ReplyAsAdminAsync( + string ticketId, + string adminId, + ReplySupportTicketRequest request, + CancellationToken cancellationToken = default) + { + var ticket = await _db.SupportTickets.FirstOrDefaultAsync(t => t.Id == ticketId, cancellationToken); + if (ticket is null) return null; + if (ticket.Status is SupportTicketStatus.Closed) + return null; + + await AddMessageAsync(ticketId, TicketMessageSenderKind.Admin, adminId, request.Body, cancellationToken); + ticket.Status = SupportTicketStatus.WaitingMerchant; + ticket.AssignedAdminId ??= adminId; + ticket.UpdatedAt = DateTime.UtcNow; + await _db.SaveChangesAsync(cancellationToken); + return await GetAdminAsync(ticketId, cancellationToken); + } + + public async Task UpdateAdminAsync( + string ticketId, + UpdateSupportTicketRequest request, + CancellationToken cancellationToken = default) + { + var ticket = await _db.SupportTickets.FirstOrDefaultAsync(t => t.Id == ticketId, cancellationToken); + if (ticket is null) return null; + + if (request.Status.HasValue) + { + ticket.Status = request.Status.Value; + if (request.Status is SupportTicketStatus.Closed or SupportTicketStatus.Resolved) + ticket.ClosedAt = DateTime.UtcNow; + } + + if (request.Priority.HasValue) + ticket.Priority = request.Priority.Value; + if (request.AssignedAdminId is not null) + ticket.AssignedAdminId = request.AssignedAdminId; + + ticket.UpdatedAt = DateTime.UtcNow; + await _db.SaveChangesAsync(cancellationToken); + return await GetAdminAsync(ticketId, cancellationToken); + } + + private IQueryable QueryTickets() => + _db.SupportTickets + .AsNoTracking() + .Select(t => new SupportTicketDto( + t.Id, + t.CafeId, + t.Cafe != null ? t.Cafe.Name : "", + t.Subject, + t.Status, + t.Priority, + t.CreatedByEmployeeId, + t.CreatedByEmployee != null ? t.CreatedByEmployee.Name : null, + t.AssignedAdminId, + t.CreatedAt, + t.UpdatedAt, + t.Messages.Count)); + + private static SupportTicketDto MapTicket(SupportTicket t) => + new( + t.Id, + t.CafeId, + t.Cafe?.Name ?? "", + t.Subject, + t.Status, + t.Priority, + t.CreatedByEmployeeId, + t.CreatedByEmployee?.Name, + t.AssignedAdminId, + t.CreatedAt, + t.UpdatedAt, + t.Messages.Count); + + private async Task> LoadMessagesAsync( + string ticketId, + CancellationToken cancellationToken) + { + var messages = await _db.SupportTicketMessages + .AsNoTracking() + .Where(m => m.TicketId == ticketId) + .OrderBy(m => m.CreatedAt) + .ToListAsync(cancellationToken); + + var employeeIds = messages + .Where(m => m.SenderKind == TicketMessageSenderKind.Merchant) + .Select(m => m.SenderId) + .Distinct() + .ToList(); + var adminIds = messages + .Where(m => m.SenderKind == TicketMessageSenderKind.Admin) + .Select(m => m.SenderId) + .Distinct() + .ToList(); + + var employeeNames = await _db.Employees + .AsNoTracking() + .Where(e => employeeIds.Contains(e.Id)) + .ToDictionaryAsync(e => e.Id, e => e.Name, cancellationToken); + var adminNames = await _db.SystemAdmins + .AsNoTracking() + .Where(a => adminIds.Contains(a.Id)) + .ToDictionaryAsync(a => a.Id, a => a.Name, cancellationToken); + + return messages.Select(m => new SupportTicketMessageDto( + m.Id, + m.SenderKind, + m.SenderId, + m.SenderKind == TicketMessageSenderKind.Admin + ? adminNames.GetValueOrDefault(m.SenderId) + : employeeNames.GetValueOrDefault(m.SenderId), + m.Body, + m.CreatedAt)).ToList(); + } + + private async Task AddMessageAsync( + string ticketId, + TicketMessageSenderKind kind, + string senderId, + string body, + CancellationToken cancellationToken) + { + _db.SupportTicketMessages.Add(new SupportTicketMessage + { + TicketId = ticketId, + SenderKind = kind, + SenderId = senderId, + Body = body.Trim() + }); + await _db.SaveChangesAsync(cancellationToken); + } +} diff --git a/src/Meezi.Infrastructure/Services/WebsiteService.cs b/src/Meezi.Infrastructure/Services/WebsiteService.cs new file mode 100644 index 0000000..c052a56 --- /dev/null +++ b/src/Meezi.Infrastructure/Services/WebsiteService.cs @@ -0,0 +1,126 @@ +using Meezi.Core.Entities; +using Meezi.Core.Interfaces; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; + +namespace Meezi.Infrastructure.Services; + +public class WebsiteService(AppDbContext db) : IWebsiteService +{ + public async Task<(IReadOnlyList Posts, int Total)> GetPostsAsync( + string locale, int page, int limit, CancellationToken ct = default) + { + var query = db.WebsiteBlogPosts.Where(p => p.IsPublished) + .OrderByDescending(p => p.PublishedAt); + + var total = await query.CountAsync(ct); + var posts = await query.Skip((page - 1) * limit).Take(limit).ToListAsync(ct); + + var dtos = posts.Select(p => (object)MapPostSummary(p, locale)).ToList(); + return (dtos, total); + } + + public async Task GetPostAsync(string slug, string locale, CancellationToken ct = default) + { + var post = await db.WebsiteBlogPosts + .Include(p => p.Comments.Where(c => c.IsApproved)) + .FirstOrDefaultAsync(p => p.Slug == slug && p.IsPublished, ct); + if (post is null) return null; + + // Increment view count (fire and forget) + post.ViewCount++; + await db.SaveChangesAsync(ct); + + return MapPostDetail(post, locale); + } + + public async Task> GetCommentsAsync(string slug, CancellationToken ct = default) + { + var comments = await db.WebsiteComments + .Where(c => c.PostSlug == slug && c.IsApproved) + .OrderBy(c => c.CreatedAt) + .ToListAsync(ct); + return comments.Select(c => (object)MapComment(c)).ToList(); + } + + public async Task AddCommentAsync(string slug, string authorName, string? email, + string content, string? ip, CancellationToken ct = default) + { + var postExists = await db.WebsiteBlogPosts + .AnyAsync(p => p.Slug == slug && p.IsPublished, ct); + if (!postExists) throw new InvalidOperationException("Post not found."); + + var comment = new WebsiteComment + { + PostSlug = slug, + AuthorName = authorName.Trim(), + AuthorEmail = email?.Trim(), + Content = content.Trim(), + IpAddress = ip, + IsApproved = false, // Requires moderation + }; + db.WebsiteComments.Add(comment); + await db.SaveChangesAsync(ct); + return MapComment(comment); + } + + public async Task CreateDemoRequestAsync(string contactName, string businessName, + string phone, string? email, string branchCount, string? notes, string source, + CancellationToken ct = default) + { + var req = new DemoRequest + { + ContactName = contactName.Trim(), + BusinessName = businessName.Trim(), + Phone = phone.Trim(), + Email = email?.Trim(), + BranchCount = branchCount, + Notes = notes?.Trim(), + Source = source, + }; + db.DemoRequests.Add(req); + await db.SaveChangesAsync(ct); + return new { req.Id, req.CreatedAt }; + } + + // ── Mappers ────────────────────────────────────────────────────────────── + + private static object MapPostSummary(WebsiteBlogPost p, string locale) => new + { + p.Id, + p.Slug, + Title = locale == "en" && !string.IsNullOrWhiteSpace(p.TitleEn) ? p.TitleEn : p.TitleFa, + Excerpt = locale == "en" && !string.IsNullOrWhiteSpace(p.ExcerptEn) ? p.ExcerptEn : p.ExcerptFa, + Category = locale == "en" && !string.IsNullOrWhiteSpace(p.CategoryEn) ? p.CategoryEn : p.CategoryFa, + p.Author, + p.CoverImage, + p.PublishedAt, + p.ViewCount, + p.TagsJson, + }; + + private static object MapPostDetail(WebsiteBlogPost p, string locale) => new + { + p.Id, + p.Slug, + Title = locale == "en" && !string.IsNullOrWhiteSpace(p.TitleEn) ? p.TitleEn : p.TitleFa, + Excerpt = locale == "en" && !string.IsNullOrWhiteSpace(p.ExcerptEn) ? p.ExcerptEn : p.ExcerptFa, + Content = locale == "en" && !string.IsNullOrWhiteSpace(p.ContentEn) ? p.ContentEn : p.ContentFa, + Category = locale == "en" && !string.IsNullOrWhiteSpace(p.CategoryEn) ? p.CategoryEn : p.CategoryFa, + p.Author, + p.CoverImage, + p.PublishedAt, + p.ViewCount, + p.TagsJson, + Comments = p.Comments.Select(c => MapComment(c)).ToList(), + }; + + private static object MapComment(WebsiteComment c) => new + { + c.Id, + c.AuthorName, + c.Content, + c.CreatedAt, + c.IsApproved, + }; +} diff --git a/src/Meezi.Shared/ApiResponse.cs b/src/Meezi.Shared/ApiResponse.cs new file mode 100644 index 0000000..0a2b227 --- /dev/null +++ b/src/Meezi.Shared/ApiResponse.cs @@ -0,0 +1,13 @@ +namespace Meezi.Shared; + +public record ApiResponse(bool Success, T? Data, ApiError? Error = null); + +public record ApiError(string Code, string Message, string? Field = null); + +public record PagedMeta(int Total, int Page, int PageSize); + +public record PagedApiResponse( + bool Success, + IReadOnlyList? Data, + PagedMeta? Meta, + ApiError? Error = null); diff --git a/src/Meezi.Shared/Meezi.Shared.csproj b/src/Meezi.Shared/Meezi.Shared.csproj new file mode 100644 index 0000000..6b512ec --- /dev/null +++ b/src/Meezi.Shared/Meezi.Shared.csproj @@ -0,0 +1 @@ + diff --git a/tests/Meezi.API.Tests/BranchMenuTests.cs b/tests/Meezi.API.Tests/BranchMenuTests.cs new file mode 100644 index 0000000..58acfe3 --- /dev/null +++ b/tests/Meezi.API.Tests/BranchMenuTests.cs @@ -0,0 +1,189 @@ +using Meezi.API.Models.Menu; +using Meezi.API.Services; +using Meezi.Core.Entities; +using Meezi.Core.Enums; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Xunit; + +namespace Meezi.API.Tests; + +public class BranchMenuTests +{ + private static (AppDbContext Db, BranchMenuService Menu, string CafeId, string BranchId, string ItemA, string ItemB) + CreateFixture(PlanTier tier = PlanTier.Pro) + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + var db = new AppDbContext(options); + + var cafeId = "cafe-1"; + var branchId = "branch-1"; + var catId = "cat-1"; + var itemA = "item-a"; + var itemB = "item-b"; + + db.Cafes.Add(new Cafe { Id = cafeId, Name = "Test", Slug = "test", PlanTier = tier }); + db.Branches.Add(new Branch + { + Id = branchId, + CafeId = cafeId, + Name = "Main", + IsActive = true, + UpdatedAt = DateTime.UtcNow + }); + db.MenuCategories.Add(new MenuCategory + { + Id = catId, + CafeId = cafeId, + Name = "Drinks", + NameEn = "Drinks", + SortOrder = 0, + IsActive = true + }); + db.MenuItems.AddRange( + new MenuItem + { + Id = itemA, + CafeId = cafeId, + CategoryId = catId, + Name = "Espresso", + Price = 100_000m, + IsAvailable = true + }, + new MenuItem + { + Id = itemB, + CafeId = cafeId, + CategoryId = catId, + Name = "Latte", + Price = 150_000m, + IsAvailable = true + }); + db.SaveChanges(); + + return (db, new BranchMenuService(db), cafeId, branchId, itemA, itemB); + } + + [Fact] + public async Task GetBranchMenu_ExcludesUnavailableItems() + { + var (_, menu, cafeId, branchId, itemA, itemB) = CreateFixture(); + await menu.UpsertOverrideAsync( + cafeId, branchId, itemB, + new UpsertBranchMenuOverrideRequest(false, null), + PlanTier.Pro, EmployeeRole.Manager, "user-1"); + + var result = await menu.GetBranchMenuAsync(cafeId, branchId, includeUnavailable: false); + + Assert.NotNull(result); + Assert.Single(result!); + Assert.Equal(itemA, result![0].Id); + } + + [Fact] + public async Task GetBranchMenu_AppliesPriceOverride_WhenSet() + { + var (_, menu, cafeId, branchId, itemA, _) = CreateFixture(); + await menu.UpsertOverrideAsync( + cafeId, branchId, itemA, + new UpsertBranchMenuOverrideRequest(true, 88_000m), + PlanTier.Pro, EmployeeRole.Manager, "user-1"); + + var result = await menu.GetBranchMenuAsync(cafeId, branchId, includeUnavailable: false); + + Assert.NotNull(result); + var row = Assert.Single(result!, r => r.Id == itemA); + Assert.Equal(100_000m, row.MasterPrice); + Assert.Equal(88_000m, row.EffectivePrice); + Assert.True(row.HasPriceOverride); + } + + [Fact] + public async Task GetBranchMenu_UsesMasterPrice_WhenNoOverride() + { + var (_, menu, cafeId, branchId, itemA, _) = CreateFixture(); + + var result = await menu.GetBranchMenuAsync(cafeId, branchId, includeUnavailable: false); + + var row = Assert.Single(result!, r => r.Id == itemA); + Assert.Equal(100_000m, row.MasterPrice); + Assert.Equal(100_000m, row.EffectivePrice); + Assert.False(row.IsOverridden); + } + + [Fact] + public async Task UpsertOverride_FreePlan_PriceOverride_ReturnsPlanLimitReached() + { + var (_, menu, cafeId, branchId, itemA, _) = CreateFixture(PlanTier.Free); + + var result = await menu.UpsertOverrideAsync( + cafeId, branchId, itemA, + new UpsertBranchMenuOverrideRequest(true, 90_000m), + PlanTier.Free, + EmployeeRole.Manager, + "user-1"); + + Assert.False(result.Success); + Assert.Equal("PLAN_LIMIT_REACHED", result.ErrorCode); + } + + [Fact] + public async Task UpsertOverride_ProPlan_PriceOverride_Succeeds() + { + var (_, menu, cafeId, branchId, itemA, _) = CreateFixture(PlanTier.Pro); + + var result = await menu.UpsertOverrideAsync( + cafeId, branchId, itemA, + new UpsertBranchMenuOverrideRequest(true, 90_000m), + PlanTier.Pro, + EmployeeRole.Manager, + "user-1"); + + Assert.True(result.Success); + Assert.Equal(90_000m, result.Data!.PriceOverride); + } + + [Fact] + public async Task DeleteOverride_ResetsToMasterPrice() + { + var (_, menu, cafeId, branchId, itemA, _) = CreateFixture(); + await menu.UpsertOverrideAsync( + cafeId, branchId, itemA, + new UpsertBranchMenuOverrideRequest(true, 90_000m), + PlanTier.Pro, EmployeeRole.Owner, "owner-1"); + + var deleted = await menu.DeleteOverrideAsync(cafeId, branchId, itemA); + Assert.True(deleted); + + var menuRows = await menu.GetBranchMenuAsync(cafeId, branchId, includeUnavailable: false); + var row = Assert.Single(menuRows!, r => r.Id == itemA); + Assert.Equal(100_000m, row.EffectivePrice); + Assert.False(row.IsOverridden); + } + + [Fact] + public async Task Override_UniqueConstraint_UpsertNotDuplicate() + { + var (db, menu, cafeId, branchId, itemA, _) = CreateFixture(); + await menu.UpsertOverrideAsync( + cafeId, branchId, itemA, + new UpsertBranchMenuOverrideRequest(true, 80_000m), + PlanTier.Pro, EmployeeRole.Manager, "user-1"); + await menu.UpsertOverrideAsync( + cafeId, branchId, itemA, + new UpsertBranchMenuOverrideRequest(false, 85_000m), + PlanTier.Pro, EmployeeRole.Manager, "user-1"); + + var count = await db.BranchMenuItemOverrides + .CountAsync(o => o.BranchId == branchId && o.MenuItemId == itemA); + Assert.Equal(1, count); + + var rows = await menu.GetBranchMenuAsync(cafeId, branchId, includeUnavailable: true); + Assert.Equal(2, rows!.Count); + var hidden = Assert.Single(rows, r => r.Id == itemA); + Assert.False(hidden.IsAvailable); + Assert.Equal(85_000m, hidden.EffectivePrice); + } +} diff --git a/tests/Meezi.API.Tests/BranchPhaseATests.cs b/tests/Meezi.API.Tests/BranchPhaseATests.cs new file mode 100644 index 0000000..8b858a5 --- /dev/null +++ b/tests/Meezi.API.Tests/BranchPhaseATests.cs @@ -0,0 +1,27 @@ +using Meezi.API.Models.Branches; +using Meezi.API.Validators; +using Meezi.Core.Utilities; +using Xunit; + +namespace Meezi.API.Tests; + +public class BranchPhaseATests +{ + [Fact] + public void CreateBranchRequestValidator_requires_valid_login_phone() + { + var validator = new CreateBranchRequestValidator(); + var invalid = validator.Validate(new CreateBranchRequest("شعبه", "123", null, null, null, null)); + Assert.False(invalid.IsValid); + + var valid = validator.Validate(new CreateBranchRequest("شعبه", "09121234567", "علی", null, null, null)); + Assert.True(valid.IsValid); + } + + [Fact] + public void PhoneNormalizer_aligns_branch_login_phones() + { + Assert.Equal("09121234567", PhoneNormalizer.Normalize("0912 123 4567")); + Assert.Equal("09121234567", PhoneNormalizer.Normalize("+989121234567")); + } +} diff --git a/tests/Meezi.API.Tests/BranchTableTests.cs b/tests/Meezi.API.Tests/BranchTableTests.cs new file mode 100644 index 0000000..edef03a --- /dev/null +++ b/tests/Meezi.API.Tests/BranchTableTests.cs @@ -0,0 +1,143 @@ +using Meezi.API.Models.Orders; +using Meezi.API.Models.Tables; +using Meezi.API.Services; +using Meezi.Core.Entities; +using Meezi.Core.Enums; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Xunit; + +namespace Meezi.API.Tests; + +public class BranchTableTests +{ + private sealed class NullKdsNotifier : IKdsNotifier + { + public Task NotifyOrderCreatedAsync(string cafeId, LiveOrderDto order, CancellationToken cancellationToken = default) => + Task.CompletedTask; + public Task NotifyOrderStatusChangedAsync(string cafeId, string orderId, OrderStatus status, CancellationToken cancellationToken = default) => + Task.CompletedTask; + public Task NotifyTableStatusChangedAsync(string cafeId, CancellationToken cancellationToken = default) => + Task.CompletedTask; + } + + private static (AppDbContext Db, TableService Tables, string CafeId, string BranchA, string BranchB, string ManagerId) + CreateFixture() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + var db = new AppDbContext(options); + var config = new Microsoft.Extensions.Configuration.ConfigurationBuilder().Build(); + var tables = new TableService(db, config, new NullKdsNotifier(), new BranchIdentityService(db)); + + var cafeId = "cafe-1"; + var branchA = "branch-a"; + var branchB = "branch-b"; + var managerId = "mgr-1"; + + db.Cafes.Add(new Cafe { Id = cafeId, Name = "Test", Slug = "test", PlanTier = PlanTier.Pro }); + db.Branches.AddRange( + new Branch { Id = branchA, CafeId = cafeId, Name = "A", IsActive = true, UpdatedAt = DateTime.UtcNow }, + new Branch { Id = branchB, CafeId = cafeId, Name = "B", IsActive = true, UpdatedAt = DateTime.UtcNow }); + db.Employees.Add(new Employee + { + Id = managerId, + CafeId = cafeId, + BranchId = branchA, + Name = "Manager", + Phone = "09121111111", + Role = EmployeeRole.Manager + }); + db.Tables.AddRange( + new Table { Id = "t-a", CafeId = cafeId, BranchId = branchA, Number = "1", QrCode = "qr-a" }, + new Table { Id = "t-b", CafeId = cafeId, BranchId = branchB, Number = "2", QrCode = "qr-b" }); + db.SaveChanges(); + + return (db, tables, cafeId, branchA, branchB, managerId); + } + + [Fact] + public async Task GetTables_ReturnsBranchTablesOnly() + { + var (_, tables, cafeId, branchA, _, _) = CreateFixture(); + var result = await tables.GetBranchTablesAsync(cafeId, branchA); + Assert.NotNull(result); + Assert.Single(result); + Assert.Equal("t-a", result[0].Id); + } + + [Fact] + public async Task GetTables_DoesNotReturnOtherBranchTables() + { + var (_, tables, cafeId, branchA, _, _) = CreateFixture(); + var result = await tables.GetBranchTablesAsync(cafeId, branchA); + Assert.NotNull(result); + Assert.DoesNotContain(result, t => t.Id == "t-b"); + } + + [Fact] + public async Task CreateTable_AssignsToBranch() + { + var (db, tables, cafeId, branchA, _, _) = CreateFixture(); + var created = await tables.CreateBranchTableAsync( + cafeId, + branchA, + new CreateBranchTableRequest("9", 6)); + Assert.True(created.Success); + Assert.Equal(branchA, created.Data!.BranchId); + Assert.Equal(1, await db.Tables.CountAsync(t => t.BranchId == branchA && t.Number == "9")); + } + + [Fact] + public async Task DeleteTable_WithOpenOrder_ReturnsTableHasOpenOrder() + { + var (db, tables, cafeId, branchA, _, _) = CreateFixture(); + db.Orders.Add(new Order + { + Id = "ord-1", + CafeId = cafeId, + BranchId = branchA, + TableId = "t-a", + Status = OrderStatus.Preparing, + OrderType = OrderType.DineIn, + Subtotal = 100, + Total = 100 + }); + await db.SaveChangesAsync(); + + var result = await tables.DeleteBranchTableAsync(cafeId, branchA, "t-a"); + Assert.False(result.Success); + Assert.Equal("TABLE_HAS_OPEN_ORDER", result.ErrorCode); + } + + [Fact] + public async Task DeleteSection_WithTables_ReturnsTableSectionHasTables() + { + var (db, tables, cafeId, branchA, _, _) = CreateFixture(); + var section = new TableSection + { + Id = "sec-1", + CafeId = cafeId, + BranchId = branchA, + Name = "Hall" + }; + db.TableSections.Add(section); + var table = await db.Tables.FirstAsync(t => t.Id == "t-a"); + table.SectionId = section.Id; + await db.SaveChangesAsync(); + + var result = await tables.DeleteBranchSectionAsync(cafeId, branchA, section.Id); + Assert.False(result.Success); + Assert.Equal("TABLE_SECTION_HAS_TABLES", result.ErrorCode); + } + + [Fact] + public async Task ManagerCannotAccessOtherBranchTables() + { + var (_, tables, cafeId, _, branchB, managerId) = CreateFixture(); + var allowed = await tables.CanAccessBranchAsync( + cafeId, branchB, managerId, EmployeeRole.Manager); + Assert.False(allowed); + } +} diff --git a/tests/Meezi.API.Tests/DailyReportServiceTests.cs b/tests/Meezi.API.Tests/DailyReportServiceTests.cs new file mode 100644 index 0000000..8c81553 --- /dev/null +++ b/tests/Meezi.API.Tests/DailyReportServiceTests.cs @@ -0,0 +1,203 @@ +using Meezi.API.Services; +using Meezi.Core.Entities; +using Meezi.Core.Enums; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging.Abstractions; +using Xunit; + +namespace Meezi.API.Tests; + +public class DailyReportServiceTests +{ + private static (AppDbContext Db, DailyReportService Service, string CafeId, string BranchId, string ItemId) + CreateFixture() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + var db = new AppDbContext(options); + + var cafeId = "cafe-report"; + var branchId = "branch-1"; + var branch2 = "branch-2"; + var itemId = "item-espresso"; + var catId = "cat-1"; + var userId = "emp-1"; + + db.Cafes.Add(new Cafe { Id = cafeId, Name = "R", Slug = "r", PlanTier = PlanTier.Business }); + db.Branches.Add(new Branch { Id = branchId, CafeId = cafeId, Name = "A", IsActive = true, UpdatedAt = DateTime.UtcNow }); + db.Branches.Add(new Branch { Id = branch2, CafeId = cafeId, Name = "B", IsActive = true, UpdatedAt = DateTime.UtcNow }); + db.Employees.Add(new Employee { Id = userId, CafeId = cafeId, BranchId = branchId, Name = "E", Phone = "09120000001", Role = EmployeeRole.Owner }); + db.MenuCategories.Add(new MenuCategory { Id = catId, CafeId = cafeId, Name = "C", NameEn = "C", SortOrder = 0 }); + db.MenuItems.Add(new MenuItem { Id = itemId, CafeId = cafeId, CategoryId = catId, Name = "Espresso", NameEn = "Espresso", Price = 50_000m, IsAvailable = true }); + db.SaveChanges(); + + var service = new DailyReportService(db, NullLogger.Instance); + return (db, service, cafeId, branchId, itemId); + } + + private static async Task SeedClosedOrderAsync( + AppDbContext db, + string cafeId, + string branchId, + string itemId, + DateTime createdAt, + bool voidFirstLine, + IReadOnlyList<(PaymentMethod Method, decimal Amount)>? payments = null, + int activeQty = 1) + { + var orderId = $"ord_{Guid.NewGuid():N}"[..16]; + var activeRevenue = 50_000m * activeQty; + var order = new Order + { + Id = orderId, + CafeId = cafeId, + BranchId = branchId, + Status = OrderStatus.Delivered, + OrderType = OrderType.DineIn, + Subtotal = activeRevenue, + TaxTotal = 0, + Total = activeRevenue, + CreatedAt = createdAt + }; + db.Orders.Add(order); + if (voidFirstLine) + { + db.OrderItems.Add(new OrderItem + { + OrderId = orderId, + MenuItemId = itemId, + Quantity = 2, + UnitPrice = 50_000m, + IsVoided = true, + VoidedAt = createdAt + }); + } + + db.OrderItems.Add(new OrderItem + { + OrderId = orderId, + MenuItemId = itemId, + Quantity = activeQty, + UnitPrice = 50_000m + }); + + if (payments is not null) + { + var shiftId = $"shift_{Guid.NewGuid():N}"[..16]; + db.RegisterShifts.Add(new Shift + { + Id = shiftId, + CafeId = cafeId, + BranchId = branchId, + OpenedByUserId = "emp-1", + OpenedAt = createdAt.AddHours(-1), + OpeningCash = 0, + Status = ShiftStatus.Open, + CreatedAt = createdAt.AddHours(-1) + }); + foreach (var p in payments) + { + db.CashTransactions.Add(new CashTransaction + { + CafeId = cafeId, + BranchId = branchId, + ShiftId = shiftId, + Type = CashTransactionType.OrderPayment, + Method = p.Method, + Amount = p.Amount, + ReferenceId = orderId, + CreatedByUserId = "emp-1", + CreatedAt = createdAt.AddMinutes(5) + }); + } + } + + await db.SaveChangesAsync(); + } + + [Fact] + public async Task GenerateReport_IsIdempotent() + { + var (db, service, cafeId, branchId, itemId) = CreateFixture(); + var date = IranCalendar.TodayInIran.AddDays(-1); + var (start, _) = IranCalendar.GetUtcRangeForIranDay(date); + await SeedClosedOrderAsync(db, cafeId, branchId, itemId, start.AddHours(12), voidFirstLine: false); + + var first = await service.GenerateReportAsync(cafeId, branchId, date); + var second = await service.GenerateReportAsync(cafeId, branchId, date); + + Assert.Equal(first.Id, second.Id); + Assert.Equal(first.TotalRevenue, second.TotalRevenue); + Assert.Equal(1, await db.DailyReports.CountAsync(r => r.CafeId == cafeId && r.BranchId == branchId && r.Date == date)); + } + + [Fact] + public async Task GenerateReport_ExcludesVoidedItemsFromRevenue() + { + var (db, service, cafeId, branchId, itemId) = CreateFixture(); + var date = IranCalendar.TodayInIran; + var (start, _) = IranCalendar.GetUtcRangeForIranDay(date); + await SeedClosedOrderAsync(db, cafeId, branchId, itemId, start.AddHours(10), voidFirstLine: true); + + var report = await service.GenerateReportAsync(cafeId, branchId, date); + + Assert.Equal(50_000m, report.TotalRevenue); + Assert.Equal(1, report.TotalVoids); + Assert.Equal(100_000m, report.VoidAmount); + } + + [Fact] + public async Task GenerateReport_SumsPaymentMethods_FromCashTransactions() + { + var (db, service, cafeId, branchId, itemId) = CreateFixture(); + var date = IranCalendar.TodayInIran; + var (start, _) = IranCalendar.GetUtcRangeForIranDay(date); + await SeedClosedOrderAsync( + db, cafeId, branchId, itemId, start.AddHours(11), false, + payments: + [ + (PaymentMethod.Cash, 60_000m), + (PaymentMethod.Card, 40_000m), + (PaymentMethod.Credit, 50_000m) + ]); + + var report = await service.GenerateReportAsync(cafeId, branchId, date); + + Assert.Equal(60_000m, report.CashRevenue); + Assert.Equal(40_000m, report.CardRevenue); + Assert.Equal(50_000m, report.CreditRevenue); + } + + [Fact] + public async Task GetSummary_AggregatesAcrossBranches() + { + var (db, service, cafeId, branchId, itemId) = CreateFixture(); + var branch2 = "branch-2"; + var date = IranCalendar.TodayInIran; + var (start, _) = IranCalendar.GetUtcRangeForIranDay(date); + + await SeedClosedOrderAsync(db, cafeId, branchId, itemId, start.AddHours(9), false); + await SeedClosedOrderAsync(db, cafeId, branch2, itemId, start.AddHours(10), false); + + await service.GenerateReportAsync(cafeId, branchId, date); + await service.GenerateReportAsync(cafeId, branch2, date); + + var summary = await service.GetSummaryAsync(cafeId, 30); + + Assert.Equal(2, summary.ByBranch.Count); + Assert.Equal(100_000m, summary.TotalRevenue); + Assert.Equal(100_000m, summary.ByBranch.Sum(b => b.TotalRevenue)); + Assert.Equal(2, summary.TotalOrders); + } + + [Fact] + public void ReportPlanGate_Free_AllowsEightDayWindow() + { + var today = new DateOnly(2026, 5, 21); + Assert.True(ReportPlanGate.IsDateInRange(PlanTier.Free, today, today)); + Assert.True(ReportPlanGate.IsDateInRange(PlanTier.Free, today.AddDays(-7), today)); + Assert.False(ReportPlanGate.IsDateInRange(PlanTier.Free, today.AddDays(-8), today)); + } +} diff --git a/tests/Meezi.API.Tests/DeliveryIntegrationTests.cs b/tests/Meezi.API.Tests/DeliveryIntegrationTests.cs new file mode 100644 index 0000000..b853c36 --- /dev/null +++ b/tests/Meezi.API.Tests/DeliveryIntegrationTests.cs @@ -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() + .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() + .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}")); + } +} diff --git a/tests/Meezi.API.Tests/DeliveryTestDoubles.cs b/tests/Meezi.API.Tests/DeliveryTestDoubles.cs new file mode 100644 index 0000000..0327053 --- /dev/null +++ b/tests/Meezi.API.Tests/DeliveryTestDoubles.cs @@ -0,0 +1,30 @@ +using Meezi.API.Services.Delivery; +using Meezi.Core.Enums; +using Meezi.Core.Interfaces; + +namespace Meezi.API.Tests; + +internal sealed class NoOpSnappfood : ISnappfoodClient +{ + public Task AcknowledgeOrderAsync(string snappfoodOrderId, CancellationToken cancellationToken = default) => + Task.CompletedTask; + public Task NotifyOrderDeliveredAsync(string snappfoodOrderId, CancellationToken cancellationToken = default) => + Task.CompletedTask; + public Task NotifyOrderStatusAsync(string snappfoodOrderId, string status, CancellationToken cancellationToken = default) => + Task.CompletedTask; +} + +internal sealed class NoOpDeliverySync : IDeliveryStatusSyncService +{ + public Task SyncInternalStatusAsync( + string cafeId, + string orderId, + OrderStatus newStatus, + CancellationToken ct = default) => Task.FromResult(false); + + public Task ApplyPlatformStatusAsync( + DeliveryPlatform platform, + string externalOrderId, + string platformStatus, + CancellationToken ct = default) => Task.FromResult(false); +} diff --git a/tests/Meezi.API.Tests/DiscoverFilterTests.cs b/tests/Meezi.API.Tests/DiscoverFilterTests.cs new file mode 100644 index 0000000..ae0dc54 --- /dev/null +++ b/tests/Meezi.API.Tests/DiscoverFilterTests.cs @@ -0,0 +1,54 @@ +using Meezi.API.Services; +using Meezi.Core.Discover; + +namespace Meezi.API.Tests; + +public class DiscoverFilterTests +{ + [Fact] + public void Matches_WhenThemeOverlaps() + { + var profile = new CafeDiscoverProfile { Themes = ["modern", "cozy"] }; + var filters = new DiscoverFilterParams(Themes: ["modern"]); + Assert.True(DiscoverProfileMatcher.Matches(profile, filters)); + } + + [Fact] + public void Rejects_WhenThemeMissing() + { + var profile = new CafeDiscoverProfile { Themes = ["vintage"] }; + var filters = new DiscoverFilterParams(Themes: ["modern"]); + Assert.False(DiscoverProfileMatcher.Matches(profile, filters)); + } + + [Fact] + public void RequireProfile_RejectsEmptyProfile() + { + var profile = new CafeDiscoverProfile(); + var filters = new DiscoverFilterParams(RequireProfile: true); + Assert.False(DiscoverProfileMatcher.Matches(profile, filters)); + } + + [Fact] + public void FromQuery_ParsesCommaSeparated() + { + var f = DiscoverFilterParams.FromQuery( + city: "تهران", + q: null, + minRating: 4, + sort: "rating", + themes: "modern, cozy", + vibes: "romantic", + occasions: null, + spaceFeatures: "outdoor", + noise: "quiet", + priceTier: "mid", + size: null, + requireProfile: true); + Assert.Equal("تهران", f.City); + Assert.Equal(4, f.MinRating); + Assert.Contains("modern", f.Themes!); + Assert.Contains("cozy", f.Themes!); + Assert.Equal("quiet", f.NoiseLevel); + } +} diff --git a/tests/Meezi.API.Tests/ExpenseServiceTests.cs b/tests/Meezi.API.Tests/ExpenseServiceTests.cs new file mode 100644 index 0000000..ebcb610 --- /dev/null +++ b/tests/Meezi.API.Tests/ExpenseServiceTests.cs @@ -0,0 +1,159 @@ +using Meezi.API.Models.Expenses; +using Meezi.API.Services; +using Meezi.Core.Entities; +using Meezi.Core.Enums; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Xunit; + +namespace Meezi.API.Tests; + +public class ExpenseServiceTests +{ + private static ( + AppDbContext Db, + ExpenseService Expenses, + ShiftService Shifts, + DailyReportService Reports, + string CafeId, + string BranchId, + string UserId) + CreateFixture() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + var db = new AppDbContext(options); + + var cafeId = "cafe-exp"; + var branchId = "branch-exp"; + var userId = "mgr-1"; + + db.Cafes.Add(new Cafe { Id = cafeId, Name = "Exp Cafe", Slug = "exp-cafe", PlanTier = PlanTier.Pro }); + db.Branches.Add(new Branch + { + Id = branchId, + CafeId = cafeId, + Name = "Main", + IsActive = true, + UpdatedAt = DateTime.UtcNow + }); + db.Employees.Add(new Employee + { + Id = userId, + CafeId = cafeId, + BranchId = branchId, + Name = "Manager", + Phone = "09123333333", + Role = EmployeeRole.Manager + }); + db.SaveChanges(); + + var shifts = new ShiftService(db); + var expenses = new ExpenseService(db, shifts); + var reports = new DailyReportService(db, Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance); + return (db, expenses, shifts, reports, cafeId, branchId, userId); + } + + [Fact] + public async Task CreateExpense_WithOpenShift_RecordsWithdrawal_AndReducesExpectedCash() + { + var (_, expenses, shifts, _, cafeId, branchId, userId) = CreateFixture(); + + var opened = await shifts.OpenShiftAsync(cafeId, branchId, 1_000_000m, userId); + var shiftId = opened.Data!.Id; + + var created = await expenses.CreateExpenseAsync( + cafeId, + new CreateExpenseRequest(branchId, shiftId, ExpenseCategory.Supplies, 150_000m, "خرید شیر", null), + userId); + + Assert.True(created.Success); + Assert.Equal(150_000m, created.Data!.Amount); + + var shift = await shifts.CloseShiftAsync(cafeId, shiftId, 850_000m, userId); + Assert.True(shift.Success); + Assert.Equal(850_000m, shift.Data!.ExpectedCash); + } + + [Fact] + public async Task DeleteExpense_SoftDeletesExpenseAndWithdrawal() + { + var (db, expenses, shifts, _, cafeId, branchId, userId) = CreateFixture(); + + var opened = await shifts.OpenShiftAsync(cafeId, branchId, 500_000m, userId); + var shiftId = opened.Data!.Id; + + var created = await expenses.CreateExpenseAsync( + cafeId, + new CreateExpenseRequest(branchId, shiftId, ExpenseCategory.Other, 50_000m, null, null), + userId); + Assert.True(created.Success); + + var deleted = await expenses.DeleteExpenseAsync(cafeId, created.Data!.Id); + Assert.True(deleted.Success); + + var expenseRow = await db.Expenses.IgnoreQueryFilters() + .FirstAsync(e => e.Id == created.Data.Id); + Assert.NotNull(expenseRow.DeletedAt); + + var tx = await db.CashTransactions.IgnoreQueryFilters() + .FirstAsync(t => t.ReferenceId == created.Data.Id); + Assert.NotNull(tx.DeletedAt); + } + + [Fact] + public async Task DailyReport_Expense_ReducesNetIncome_NotRevenue() + { + var (db, expenses, shifts, reports, cafeId, branchId, userId) = CreateFixture(); + var date = IranCalendar.TodayInIran; + var (start, _) = IranCalendar.GetUtcRangeForIranDay(date); + + var catId = "cat-1"; + var itemId = "item-1"; + db.MenuCategories.Add(new MenuCategory { Id = catId, CafeId = cafeId, Name = "C", NameEn = "C", SortOrder = 0 }); + db.MenuItems.Add(new MenuItem + { + Id = itemId, + CafeId = cafeId, + CategoryId = catId, + Name = "Tea", + NameEn = "Tea", + Price = 100_000m, + IsAvailable = true + }); + + var orderId = "ord-1"; + db.Orders.Add(new Order + { + Id = orderId, + CafeId = cafeId, + BranchId = branchId, + Status = OrderStatus.Delivered, + OrderType = OrderType.DineIn, + Subtotal = 100_000m, + TaxTotal = 0, + Total = 100_000m, + CreatedAt = start.AddHours(10) + }); + db.OrderItems.Add(new OrderItem + { + OrderId = orderId, + MenuItemId = itemId, + Quantity = 1, + UnitPrice = 100_000m + }); + await db.SaveChangesAsync(); + + await expenses.CreateExpenseAsync( + cafeId, + new CreateExpenseRequest(branchId, null, ExpenseCategory.Utilities, 30_000m, "برق", null), + userId); + + var report = await reports.GenerateReportAsync(cafeId, branchId, date); + + Assert.Equal(100_000m, report.TotalRevenue); + Assert.Equal(30_000m, report.TotalExpenses); + Assert.Equal(70_000m, report.NetIncome); + } +} diff --git a/tests/Meezi.API.Tests/Integration/HealthIntegrationTests.cs b/tests/Meezi.API.Tests/Integration/HealthIntegrationTests.cs new file mode 100644 index 0000000..bfb2750 --- /dev/null +++ b/tests/Meezi.API.Tests/Integration/HealthIntegrationTests.cs @@ -0,0 +1,20 @@ +using System.Net; +using Xunit; + +namespace Meezi.API.Tests.Integration; + +[Collection(nameof(MeeziIntegrationCollection))] +public class HealthIntegrationTests +{ + private readonly MeeziWebApplicationFactory _factory; + + public HealthIntegrationTests(MeeziWebApplicationFactory factory) => _factory = factory; + + [Fact] + public async Task Health_ReturnsOk() + { + using var client = _factory.CreateClient(); + var response = await client.GetAsync("/health"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } +} diff --git a/tests/Meezi.API.Tests/Integration/MeeziIntegrationCollection.cs b/tests/Meezi.API.Tests/Integration/MeeziIntegrationCollection.cs new file mode 100644 index 0000000..6aa4592 --- /dev/null +++ b/tests/Meezi.API.Tests/Integration/MeeziIntegrationCollection.cs @@ -0,0 +1,6 @@ +using Xunit; + +namespace Meezi.API.Tests.Integration; + +[CollectionDefinition(nameof(MeeziIntegrationCollection))] +public class MeeziIntegrationCollection : ICollectionFixture; diff --git a/tests/Meezi.API.Tests/Integration/MeeziWebApplicationFactory.cs b/tests/Meezi.API.Tests/Integration/MeeziWebApplicationFactory.cs new file mode 100644 index 0000000..5dd8bf0 --- /dev/null +++ b/tests/Meezi.API.Tests/Integration/MeeziWebApplicationFactory.cs @@ -0,0 +1,49 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.AspNetCore.TestHost; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Meezi.API; +using Meezi.Infrastructure.Data; + +namespace Meezi.API.Tests.Integration; + +public class MeeziWebApplicationFactory : WebApplicationFactory +{ + protected override IHost CreateHost(IHostBuilder builder) + { + var app = Program.BuildWebApplication( + Array.Empty(), + ConfigureTestingBeforeServices, + ConfigureTestingAfterServices); + app.StartAsync().GetAwaiter().GetResult(); + return app; + } + + private static void ConfigureTestingBeforeServices(WebApplicationBuilder webBuilder) + { + webBuilder.Environment.EnvironmentName = "Testing"; + webBuilder.WebHost.UseTestServer(); + webBuilder.Configuration.AddInMemoryCollection(new Dictionary + { + ["Testing:Enabled"] = "true", + ["Testing:SkipSeed"] = "true", + ["RUN_MIGRATIONS"] = "false", + ["ConnectionStrings:Redis"] = "127.0.0.1:6379" + }); + } + + private static void ConfigureTestingAfterServices(WebApplicationBuilder webBuilder) + { + var dbDescriptor = webBuilder.Services.SingleOrDefault(d => + d.ServiceType == typeof(DbContextOptions)); + if (dbDescriptor is not null) + webBuilder.Services.Remove(dbDescriptor); + + webBuilder.Services.AddDbContext(options => + options.UseInMemoryDatabase("meezi_test_db")); + } +} diff --git a/tests/Meezi.API.Tests/Meezi.API.Tests.csproj b/tests/Meezi.API.Tests/Meezi.API.Tests.csproj new file mode 100644 index 0000000..c0c9ecf --- /dev/null +++ b/tests/Meezi.API.Tests/Meezi.API.Tests.csproj @@ -0,0 +1,24 @@ + + + + false + + + + + + + + + + + + + + + + + + + + diff --git a/tests/Meezi.API.Tests/NoOpInventoryService.cs b/tests/Meezi.API.Tests/NoOpInventoryService.cs new file mode 100644 index 0000000..d894f66 --- /dev/null +++ b/tests/Meezi.API.Tests/NoOpInventoryService.cs @@ -0,0 +1,34 @@ +using Meezi.API.Services; + +namespace Meezi.API.Tests; + +internal sealed class NoOpInventoryService : IInventoryService +{ + public Task> ListAsync(string cafeId, CancellationToken ct = default) => + Task.FromResult>([]); + + public Task> LowStockAsync(string cafeId, CancellationToken ct = default) => + Task.FromResult>([]); + + public Task CreateAsync(string cafeId, CreateIngredientRequest request, CancellationToken ct = default) => + Task.FromResult(null); + + public Task UpdateAsync(string cafeId, string ingredientId, UpdateIngredientRequest request, CancellationToken ct = default) => + Task.FromResult(null); + + public Task AdjustAsync(string cafeId, string ingredientId, AdjustStockRequest request, CancellationToken ct = default) => + Task.FromResult(null); + + public Task GetRecipeAsync(string cafeId, string menuItemId, CancellationToken ct = default) => + Task.FromResult(null); + + public Task SetRecipeAsync(string cafeId, string menuItemId, SetMenuItemRecipeRequest request, CancellationToken ct = default) => + Task.FromResult(null); + + public Task DeductForOrderAsync( + string cafeId, + string orderId, + IReadOnlyList<(string MenuItemId, int Quantity)> lines, + CancellationToken ct = default) => + Task.FromResult(new OrderDeductionResult(false, [])); +} diff --git a/tests/Meezi.API.Tests/NoOpLoyaltyService.cs b/tests/Meezi.API.Tests/NoOpLoyaltyService.cs new file mode 100644 index 0000000..cfc2f35 --- /dev/null +++ b/tests/Meezi.API.Tests/NoOpLoyaltyService.cs @@ -0,0 +1,13 @@ +using Meezi.API.Services; + +namespace Meezi.API.Tests; + +internal sealed class NoOpLoyaltyService : ILoyaltyService +{ + public Task ApplyEarnOnOrderPaidAsync( + string cafeId, + string? customerId, + decimal paidAmount, + CancellationToken ct = default) => + Task.CompletedTask; +} diff --git a/tests/Meezi.API.Tests/NoOpOrderNotificationService.cs b/tests/Meezi.API.Tests/NoOpOrderNotificationService.cs new file mode 100644 index 0000000..b2deae8 --- /dev/null +++ b/tests/Meezi.API.Tests/NoOpOrderNotificationService.cs @@ -0,0 +1,14 @@ +using Meezi.API.Models.Orders; +using Meezi.API.Services; +using Meezi.Core.Entities; + +namespace Meezi.API.Tests; + +internal sealed class NoOpOrderNotificationService : IOrderNotificationService +{ + public Task NotifyGuestOrderPlacedAsync(Order order, LiveOrderDto live, CancellationToken ct = default) => + Task.CompletedTask; + + public Task NotifyOrderStatusChangedAsync(Order order, CancellationToken ct = default) => + Task.CompletedTask; +} diff --git a/tests/Meezi.API.Tests/OrderSessionTests.cs b/tests/Meezi.API.Tests/OrderSessionTests.cs new file mode 100644 index 0000000..f9adf53 --- /dev/null +++ b/tests/Meezi.API.Tests/OrderSessionTests.cs @@ -0,0 +1,226 @@ +using Meezi.API.Models.Orders; +using Meezi.API.Services; +using LiveOrderDto = Meezi.API.Models.Orders.LiveOrderDto; +using Meezi.Core.Entities; +using Meezi.Core.Enums; +using Meezi.Core.Interfaces; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Xunit; + +namespace Meezi.API.Tests; + +public class OrderSessionTests +{ + private sealed class NullKdsNotifier : IKdsNotifier + { + public Task NotifyOrderCreatedAsync(string cafeId, LiveOrderDto order, CancellationToken cancellationToken = default) => + Task.CompletedTask; + public Task NotifyOrderStatusChangedAsync(string cafeId, string orderId, OrderStatus status, CancellationToken cancellationToken = default) => + Task.CompletedTask; + public Task NotifyTableStatusChangedAsync(string cafeId, CancellationToken cancellationToken = default) => + Task.CompletedTask; + } + + private static (AppDbContext Db, OrderService Orders, TableService Tables, string CafeId, string TableId, string MenuItemId) + CreateFixture() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + var db = new AppDbContext(options); + + var cafeId = "cafe-1"; + var branchId = "branch-1"; + var tableId = "table-1"; + var menuItemId = "item-1"; + var categoryId = "cat-1"; + var userId = "user-1"; + + db.Cafes.Add(new Cafe { Id = cafeId, Name = "Test", Slug = "test", PlanTier = PlanTier.Pro }); + db.Branches.Add(new Branch + { + Id = branchId, + CafeId = cafeId, + Name = "Main", + IsActive = true, + UpdatedAt = DateTime.UtcNow + }); + db.Employees.Add(new Employee + { + Id = userId, + CafeId = cafeId, + BranchId = branchId, + Name = "Cashier", + Phone = "09121111111", + Role = EmployeeRole.Cashier + }); + db.Tables.Add(new Table + { + Id = tableId, + CafeId = cafeId, + BranchId = branchId, + Number = "5", + QrCode = "qr1", + Capacity = 4 + }); + db.MenuCategories.Add(new MenuCategory + { + Id = categoryId, + CafeId = cafeId, + Name = "Drinks", + NameEn = "Drinks", + SortOrder = 0 + }); + db.MenuItems.Add(new MenuItem + { + Id = menuItemId, + CafeId = cafeId, + CategoryId = categoryId, + Name = "Espresso", + NameEn = "Espresso", + Price = 100_000m, + IsAvailable = true + }); + db.SaveChanges(); + + var shifts = new ShiftService(db); + shifts.OpenShiftAsync(cafeId, branchId, 0m, userId).GetAwaiter().GetResult(); + + var kds = new NoOpKdsNotifier(); + var snapp = new NoOpSnappfood(); + var orders = new OrderService(db, kds, snapp, new NoOpDeliverySync(), shifts, TestServiceScopeFactory.Create(), new NoOpOrderNotificationService(), new NoOpInventoryService(), new NoOpLoyaltyService()); + var tables = new TableService( + db, + new Microsoft.Extensions.Configuration.ConfigurationBuilder().Build(), + new NullKdsNotifier(), + new BranchIdentityService(db)); + + return (db, orders, tables, cafeId, tableId, menuItemId); + } + + private static CreateOrderRequest DineInRequest(string tableId, string menuItemId, int qty = 1) => + new( + OrderType.DineIn, + "branch-1", + tableId, + null, + "Ali", + "09121111111", + null, + null, + [new CreateOrderItemRequest(menuItemId, qty, null)]); + + [Fact] + public async Task CreateOrder_SameTable_MergesIntoSingleOpenOrder() + { + var (_, orders, _, cafeId, tableId, menuItemId) = CreateFixture(); + var tenant = new TestTenant(cafeId, "user-1"); + + var first = await orders.CreateOrderAsync(cafeId, tenant, DineInRequest(tableId, menuItemId), default); + var second = await orders.CreateOrderAsync(cafeId, tenant, DineInRequest(tableId, menuItemId, 2), default); + + Assert.True(first.Success); + Assert.True(second.Success); + Assert.Equal(first.Data!.Id, second.Data!.Id); + Assert.Equal(3, second.Data!.Items.Sum(i => i.Quantity)); + } + + [Fact] + public async Task AppendItems_IncreasesTotal() + { + var (_, orders, _, cafeId, tableId, menuItemId) = CreateFixture(); + var tenant = new TestTenant(cafeId, "user-1"); + + var created = await orders.CreateOrderAsync(cafeId, tenant, DineInRequest(tableId, menuItemId), default); + var append = await orders.AppendOrderItemsAsync( + cafeId, + created.Data!.Id, + new AppendOrderItemsRequest([new CreateOrderItemRequest(menuItemId, 1, null)]), + default); + + Assert.True(append.Success); + Assert.Equal(200_000m, append.Data!.Subtotal); + } + + [Fact] + public async Task GetOpenOrders_SearchByPhone_FindsOrder() + { + var (_, orders, _, cafeId, tableId, menuItemId) = CreateFixture(); + var tenant = new TestTenant(cafeId, "user-1"); + await orders.CreateOrderAsync(cafeId, tenant, DineInRequest(tableId, menuItemId), default); + + var found = await orders.GetOpenOrdersAsync(cafeId, "09121111111", default); + Assert.Single(found); + Assert.Equal("09121111111", found[0].GuestPhone); + } + + [Fact] + public async Task Payment_CompletesOrder_TableBoardShowsFree() + { + var (_, orders, tables, cafeId, tableId, menuItemId) = CreateFixture(); + var tenant = new TestTenant(cafeId, "user-1"); + + var created = await orders.CreateOrderAsync(cafeId, tenant, DineInRequest(tableId, menuItemId), default); + var order = created.Data!; + + var paid = await orders.RecordPaymentsAsync( + cafeId, + order.Id, + new RecordPaymentsRequest([new CreatePaymentRequest(PaymentMethod.Cash, order.Total, null)]), + "user-1", + default); + Assert.True(paid.Success); + + var board = await tables.GetTableBoardAsync(cafeId, cancellationToken: default); + var tile = board.First(t => t.Id == tableId); + Assert.Equal(TableBoardStatus.Free, tile.Status); + } + + [Fact] + public async Task CleaningTable_BlocksNewOrder() + { + var (db, orders, tables, cafeId, tableId, menuItemId) = CreateFixture(); + var table = await db.Tables.FindAsync(tableId); + table!.IsCleaning = true; + await db.SaveChangesAsync(); + + var tenant = new TestTenant(cafeId, "user-1"); + var result = await orders.CreateOrderAsync(cafeId, tenant, DineInRequest(tableId, menuItemId), default); + + Assert.False(result.Success); + Assert.Equal("TABLE_NOT_AVAILABLE", result.ErrorCode); + } + + private sealed class TestTenant(string cafeId, string userId) : ITenantContext + { + public string? CafeId => cafeId; + public string? UserId => userId; + public EmployeeRole? Role => EmployeeRole.Owner; + public PlanTier? PlanTier => Core.Enums.PlanTier.Pro; + public string? Language => "fa"; + public string? BranchId => null; + public bool IsSystemAdmin => false; + public bool IsAuthenticated => true; + } + + private sealed class NoOpKdsNotifier : IKdsNotifier + { + public Task NotifyOrderCreatedAsync(string cafeId, LiveOrderDto order, CancellationToken cancellationToken = default) => + Task.CompletedTask; + public Task NotifyOrderStatusChangedAsync(string cafeId, string orderId, OrderStatus status, CancellationToken cancellationToken = default) => + Task.CompletedTask; + public Task NotifyTableStatusChangedAsync(string cafeId, CancellationToken cancellationToken = default) => + Task.CompletedTask; + } + + private sealed class NoOpSnappfood : ISnappfoodClient + { + public Task AcknowledgeOrderAsync(string snappfoodOrderId, CancellationToken cancellationToken = default) => + Task.CompletedTask; + public Task NotifyOrderDeliveredAsync(string snappfoodOrderId, CancellationToken cancellationToken = default) => + Task.CompletedTask; + public Task NotifyOrderStatusAsync(string snappfoodOrderId, string status, CancellationToken cancellationToken = default) => + Task.CompletedTask; + } +} diff --git a/tests/Meezi.API.Tests/OrderVoidTransferTests.cs b/tests/Meezi.API.Tests/OrderVoidTransferTests.cs new file mode 100644 index 0000000..0f47bc3 --- /dev/null +++ b/tests/Meezi.API.Tests/OrderVoidTransferTests.cs @@ -0,0 +1,212 @@ +using Meezi.API.Models.Orders; +using Meezi.API.Models.Shifts; +using Meezi.API.Services; +using Meezi.Core.Entities; +using Meezi.Core.Enums; +using Meezi.Core.Interfaces; +using Meezi.Infrastructure.Data; +using TenantContext = Meezi.Infrastructure.Data.TenantContext; +using Microsoft.EntityFrameworkCore; +using Xunit; + +namespace Meezi.API.Tests; + +public class OrderVoidTransferTests +{ + private static ( + AppDbContext Db, + OrderService Orders, + string CafeId, + string Table1Id, + string Table2Id, + string MenuItemId, + TenantContext Tenant) + CreateFixture() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + var db = new AppDbContext(options); + + var cafeId = "cafe-void"; + var table1 = "table-1"; + var table2 = "table-2"; + var menuItemId = "item-1"; + var categoryId = "cat-1"; + var managerId = "emp-manager"; + + db.Cafes.Add(new Cafe { Id = cafeId, Name = "Test", Slug = "test-void", PlanTier = PlanTier.Pro }); + db.Tables.Add(new Table { Id = table1, CafeId = cafeId, Number = "1", QrCode = "qr1", Capacity = 4 }); + db.Tables.Add(new Table { Id = table2, CafeId = cafeId, Number = "2", QrCode = "qr2", Capacity = 4 }); + db.MenuCategories.Add(new MenuCategory + { + Id = categoryId, + CafeId = cafeId, + Name = "Drinks", + NameEn = "Drinks", + SortOrder = 0 + }); + db.MenuItems.Add(new MenuItem + { + Id = menuItemId, + CafeId = cafeId, + CategoryId = categoryId, + Name = "Tea", + NameEn = "Tea", + Price = 50_000m, + IsAvailable = true + }); + db.Employees.Add(new Employee + { + Id = managerId, + CafeId = cafeId, + Name = "Manager", + Phone = "09120000000", + Role = EmployeeRole.Manager + }); + db.SaveChanges(); + + var tenant = new TenantContext + { + CafeId = cafeId, + UserId = managerId, + Role = EmployeeRole.Manager, + PlanTier = PlanTier.Pro, + Language = "fa" + }; + + var orders = new OrderService(db, new NoOpKdsNotifier(), new NoOpSnappfood(), new NoOpDeliverySync(), new NoOpShiftService(), TestServiceScopeFactory.Create(), new NoOpOrderNotificationService(), new NoOpInventoryService(), new NoOpLoyaltyService()); + return (db, orders, cafeId, table1, table2, menuItemId, tenant); + } + + private static async Task CreateOpenOrderAsync( + OrderService orders, + string cafeId, + TenantContext tenant, + string tableId, + string menuItemId, + int qty = 1) + { + var result = await orders.CreateOrderAsync( + cafeId, + tenant, + new CreateOrderRequest( + OrderType.DineIn, + null, + tableId, + null, + "Guest", + null, + null, + null, + [new CreateOrderItemRequest(menuItemId, qty, null)])); + Assert.True(result.Success); + return result.Data!; + } + + [Fact] + public async Task VoidItem_ReducesOrderTotal() + { + var (_, orders, cafeId, table1, _, menuItemId, tenant) = CreateFixture(); + var order = await CreateOpenOrderAsync(orders, cafeId, tenant, table1, menuItemId, 2); + var itemId = order.Items[0].Id; + var before = order.Total; + + var result = await orders.VoidOrderItemAsync(cafeId, order.Id, itemId, tenant.UserId!); + + Assert.True(result.Success); + Assert.True(result.Data!.Items.First(i => i.Id == itemId).IsVoided); + Assert.True(result.Data.Total < before); + } + + [Fact] + public async Task VoidItem_AlreadyVoided_ReturnsError() + { + var (_, orders, cafeId, table1, _, menuItemId, tenant) = CreateFixture(); + var order = await CreateOpenOrderAsync(orders, cafeId, tenant, table1, menuItemId); + var itemId = order.Items[0].Id; + + await orders.VoidOrderItemAsync(cafeId, order.Id, itemId, tenant.UserId!); + var second = await orders.VoidOrderItemAsync(cafeId, order.Id, itemId, tenant.UserId!); + + Assert.False(second.Success); + Assert.Equal("ITEM_ALREADY_VOIDED", second.ErrorCode); + } + + [Fact] + public async Task TransferTable_MovesOrderToFreeTable() + { + var (_, orders, cafeId, table1, table2, menuItemId, tenant) = CreateFixture(); + var order = await CreateOpenOrderAsync(orders, cafeId, tenant, table1, menuItemId); + + var result = await orders.TransferTableAsync(cafeId, order.Id, table2); + + Assert.True(result.Success); + Assert.Equal(table2, result.Data!.TableId); + } + + [Fact] + public async Task TransferTable_ToOccupiedTable_ReturnsTableOccupied() + { + var (_, orders, cafeId, table1, table2, menuItemId, tenant) = CreateFixture(); + await CreateOpenOrderAsync(orders, cafeId, tenant, table1, menuItemId); + var order2 = await CreateOpenOrderAsync(orders, cafeId, tenant, table2, menuItemId); + + var result = await orders.TransferTableAsync(cafeId, order2.Id, table1); + + Assert.False(result.Success); + Assert.Equal("TABLE_OCCUPIED", result.ErrorCode); + } + + [Fact] + public async Task TransferTable_ToCleaningTable_ReturnsTableCleaning() + { + var (db, orders, cafeId, table1, table2, menuItemId, tenant) = CreateFixture(); + var order = await CreateOpenOrderAsync(orders, cafeId, tenant, table1, menuItemId); + + var table = await db.Tables.FirstAsync(t => t.Id == table2); + table.IsCleaning = true; + await db.SaveChangesAsync(); + + var result = await orders.TransferTableAsync(cafeId, order.Id, table2); + + Assert.False(result.Success); + Assert.Equal("TABLE_CLEANING", result.ErrorCode); + } + + private sealed class NoOpKdsNotifier : IKdsNotifier + { + public Task NotifyOrderCreatedAsync(string cafeId, LiveOrderDto order, CancellationToken cancellationToken = default) => + Task.CompletedTask; + public Task NotifyOrderStatusChangedAsync(string cafeId, string orderId, OrderStatus status, CancellationToken cancellationToken = default) => + Task.CompletedTask; + public Task NotifyTableStatusChangedAsync(string cafeId, CancellationToken cancellationToken = default) => + Task.CompletedTask; + } + + private sealed class NoOpSnappfood : ISnappfoodClient + { + public Task AcknowledgeOrderAsync(string snappfoodOrderId, CancellationToken cancellationToken = default) => + Task.CompletedTask; + public Task NotifyOrderDeliveredAsync(string snappfoodOrderId, CancellationToken cancellationToken = default) => + Task.CompletedTask; + public Task NotifyOrderStatusAsync(string snappfoodOrderId, string status, CancellationToken cancellationToken = default) => + Task.CompletedTask; + } + + private sealed class NoOpShiftService : IShiftService + { + public Task> OpenShiftAsync(string cafeId, string branchId, decimal openingCash, string userId, CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + public Task> CloseShiftAsync(string cafeId, string shiftId, decimal closingCash, string userId, CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + public Task GetCurrentShiftAsync(string cafeId, string branchId, CancellationToken cancellationToken = default) => + Task.FromResult(null); + public Task?> GetTransactionsAsync(string cafeId, string shiftId, CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + public Task> RecordTransactionAsync(string cafeId, string shiftId, CashTransactionType type, PaymentMethod method, decimal amount, string createdByUserId, string? referenceId = null, string? note = null, CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + public Task> RequireOpenShiftForBranchAsync(string cafeId, string branchId, CancellationToken cancellationToken = default) => + Task.FromResult(new ShiftServiceResult(true, new Shift { Id = "shift-test", CafeId = cafeId, BranchId = branchId })); + } +} diff --git a/tests/Meezi.API.Tests/OtpNormalizerTests.cs b/tests/Meezi.API.Tests/OtpNormalizerTests.cs new file mode 100644 index 0000000..0455b0e --- /dev/null +++ b/tests/Meezi.API.Tests/OtpNormalizerTests.cs @@ -0,0 +1,27 @@ +using Meezi.Core.Utilities; +using Xunit; + +namespace Meezi.API.Tests; + +public class OtpNormalizerTests +{ + [Theory] + [InlineData("916581", "916581")] + [InlineData(" 916581 ", "916581")] + [InlineData("۹۱۶۵۸۱", "916581")] + [InlineData("٩١٦٥٨١", "916581")] + public void Normalize_maps_to_ascii_digits(string input, string expected) + { + Assert.Equal(expected, OtpNormalizer.Normalize(input)); + Assert.True(OtpNormalizer.IsValidSixDigitCode(input)); + } + + [Theory] + [InlineData("")] + [InlineData("12345")] + [InlineData("1234567")] + public void IsValid_rejects_invalid(string input) + { + Assert.False(OtpNormalizer.IsValidSixDigitCode(input)); + } +} diff --git a/tests/Meezi.API.Tests/PlanLimitsTests.cs b/tests/Meezi.API.Tests/PlanLimitsTests.cs new file mode 100644 index 0000000..05b5807 --- /dev/null +++ b/tests/Meezi.API.Tests/PlanLimitsTests.cs @@ -0,0 +1,44 @@ +using Meezi.Core.Constants; +using Meezi.Core.Enums; +using Xunit; + +namespace Meezi.API.Tests; + +public class PlanLimitsTests +{ + [Theory] + [InlineData(PlanTier.Free, 1)] + [InlineData(PlanTier.Pro, 3)] + public void MaxBranches_MatchesTier(PlanTier tier, int expected) + { + Assert.Equal(expected, PlanLimits.MaxBranches(tier)); + } + + [Fact] + public void MaxBranches_Business_IsUnlimited() + { + Assert.Equal(int.MaxValue, PlanLimits.MaxBranches(PlanTier.Business)); + } + + [Theory] + [InlineData(PlanTier.Free, 8)] + [InlineData(PlanTier.Pro, 90)] + public void MaxReportHistoryDays_MatchesTier(PlanTier tier, int expected) + { + Assert.Equal(expected, PlanLimits.MaxReportHistoryDays(tier)); + } + + [Fact] + public void MaxReportHistoryDays_Business_IsUnlimited() + { + Assert.Equal(int.MaxValue, PlanLimits.MaxReportHistoryDays(PlanTier.Business)); + } + + [Theory] + [InlineData(PlanTier.Free, 1)] + [InlineData(PlanTier.Pro, 3)] + public void MaxTerminals_MatchesTier(PlanTier tier, int expected) + { + Assert.Equal(expected, PlanLimits.MaxTerminals(tier)); + } +} diff --git a/tests/Meezi.API.Tests/PrintingTests.cs b/tests/Meezi.API.Tests/PrintingTests.cs new file mode 100644 index 0000000..fe60152 --- /dev/null +++ b/tests/Meezi.API.Tests/PrintingTests.cs @@ -0,0 +1,120 @@ +using Meezi.API.Models.Orders; +using Meezi.API.Services.Printing; +using Meezi.Core.Enums; +using Xunit; + +namespace Meezi.API.Tests; + +public class PrintingTests +{ + private static OrderDto SampleOrder() => new( + "order-abc12345", + "cafe-1", + "branch-1", + "table-1", + "5", + "Ali", + null, + null, + null, + null, + null, + OrderType.DineIn, + OrderSource.Pos, + OrderStatus.Pending, + 200_000m, + 18_000m, + 0m, + 218_000m, + 0m, + DateTime.UtcNow, + [ + new OrderItemDto("i1", "m1", "Espresso", 2, 100_000m, null), + new OrderItemDto("i2", "m2", "Void Latte", 1, 50_000m, null, true) + ], + [new PaymentDto("p1", PaymentMethod.Cash, 218_000m, PaymentStatus.Completed, null)]); + + private static ReceiptPrintContext Ctx(int paper = 80) => new( + SampleOrder(), + "کافه دمو", + "شعبه اصلی", + "خوش آمدید", + "با تشکر", + "wifi1234", + paper, + true); + + [Fact] + public void ReceiptBuilder_ExcludesVoidedItems() + { + var bytes = new ReceiptBuilder().BuildReceipt(Ctx()); + var text = System.Text.Encoding.UTF8.GetString(bytes); + Assert.Contains("Espresso", text); + Assert.DoesNotContain("Void Latte", text); + } + + [Fact] + public void ReceiptBuilder_AppliesPersianCalendarDate() + { + var bytes = new ReceiptBuilder().BuildReceipt(Ctx()); + var text = System.Text.Encoding.UTF8.GetString(bytes); + Assert.Contains("تاریخ:", text); + } + + [Fact] + public void ReceiptBuilder_ShowsTaxLine_WhenTaxNonZero() + { + var bytes = new ReceiptBuilder().BuildReceipt(Ctx()); + var text = System.Text.Encoding.UTF8.GetString(bytes); + Assert.Contains("مالیات", text); + } + + [Fact] + public void ReceiptBuilder_80mm_Uses48CharWidth() + { + var sep = new EscPosBuilder().Separator(48).Build(); + var receipt = new ReceiptBuilder().BuildReceipt(Ctx(80)); + Assert.NotEmpty(receipt); + Assert.Contains(sep, receipt); + } + + [Fact] + public void ReceiptBuilder_58mm_Uses32CharWidth() + { + var sep = new EscPosBuilder().Separator(32).Build(); + var receipt = new ReceiptBuilder().BuildReceipt(Ctx(58)); + Assert.Contains(sep, receipt); + } + + [Fact] + public void KitchenTicket_IncludesItemNotes() + { + var order = SampleOrder() with + { + Items = + [ + new OrderItemDto("i1", "m1", "Burger", 1, 100_000m, "بدون پیاز") + ] + }; + var ctx = Ctx() with { Order = order }; + var text = System.Text.Encoding.UTF8.GetString(new ReceiptBuilder().BuildKitchenTicket(ctx)); + Assert.Contains("بدون پیاز", text); + Assert.Contains("آشپزخانه", text); + } + + [Fact] + public void EscPosBuilder_Cut_AppendsCorrectBytes() + { + var bytes = new EscPosBuilder().Cut().Build(); + Assert.Equal([0x1D, 0x56, 0x42, 0x03], bytes); + } + + [Fact] + public void EscPosBuilder_TwoColumns_PadsCorrectly() + { + var bytes = new EscPosBuilder().TwoColumns("کالا", "1000", 10).Build(); + var line = System.Text.Encoding.UTF8.GetString(bytes).TrimEnd('\n'); + Assert.Equal(10, line.Length); + Assert.EndsWith("1000", line); + } +} diff --git a/tests/Meezi.API.Tests/QrMenuTests.cs b/tests/Meezi.API.Tests/QrMenuTests.cs new file mode 100644 index 0000000..da91e40 --- /dev/null +++ b/tests/Meezi.API.Tests/QrMenuTests.cs @@ -0,0 +1,230 @@ +using Meezi.API.Models.Menu; +using Meezi.API.Models.Orders; +using Meezi.API.Models.Public; +using Meezi.API.Services; +using Meezi.Core.Entities; +using Meezi.Core.Enums; +using Meezi.Core.Interfaces; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Xunit; +using LiveOrderDto = Meezi.API.Models.Orders.LiveOrderDto; + +namespace Meezi.API.Tests; + +public class QrMenuTests +{ + private sealed class NoOpKds : IKdsNotifier + { + public Task NotifyOrderCreatedAsync(string cafeId, LiveOrderDto order, CancellationToken ct = default) => + Task.CompletedTask; + public Task NotifyOrderStatusChangedAsync(string cafeId, string orderId, OrderStatus status, CancellationToken ct = default) => + Task.CompletedTask; + public Task NotifyTableStatusChangedAsync(string cafeId, CancellationToken ct = default) => + Task.CompletedTask; + } + + private static ( + AppDbContext Db, + TableService Tables, + PublicService Public, + string CafeId, + string BranchId, + string TableId, + string ItemA, + string ItemB, + string QrCode + ) CreateFixture() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + var db = new AppDbContext(options); + + var cafeId = "cafe-qr"; + var branchId = "branch-qr"; + var catId = "cat-qr"; + var itemA = "item-qr-a"; + var itemB = "item-qr-b"; + var tableId = "table-qr-1"; + const string qrCode = "demo_table_01"; + + db.Cafes.Add(new Cafe + { + Id = cafeId, + Name = "Demo", + Slug = "demo", + PlanTier = PlanTier.Pro, + DefaultTaxRate = 9m + }); + db.Branches.Add(new Branch + { + Id = branchId, + CafeId = cafeId, + Name = "Main", + IsActive = true, + WelcomeText = "خوش آمدید", + UpdatedAt = DateTime.UtcNow + }); + db.MenuCategories.Add(new MenuCategory + { + Id = catId, + CafeId = cafeId, + Name = "Drinks", + SortOrder = 0, + IsActive = true + }); + db.MenuItems.AddRange( + new MenuItem + { + Id = itemA, + CafeId = cafeId, + CategoryId = catId, + Name = "Espresso", + Price = 100_000m, + IsAvailable = true + }, + new MenuItem + { + Id = itemB, + CafeId = cafeId, + CategoryId = catId, + Name = "Latte", + Price = 150_000m, + IsAvailable = true + }); + db.Tables.Add(new Table + { + Id = tableId, + CafeId = cafeId, + BranchId = branchId, + Number = "1", + QrCode = qrCode, + IsActive = true + }); + db.SaveChanges(); + + var config = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary { ["App:QrPublicBaseUrl"] = "https://meezi.test" }) + .Build(); + var kds = new NoOpKds(); + var branchMenu = new BranchMenuService(db); + var identity = new BranchIdentityService(db); + var tables = new TableService(db, config, kds, identity); + var shifts = new ShiftService(db); + var orders = new OrderService(db, kds, new NoOpSnappfood(), new NoOpDeliverySync(), shifts, TestServiceScopeFactory.Create(), new NoOpOrderNotificationService(), new NoOpInventoryService(), new NoOpLoyaltyService()); + var publicSvc = new PublicService(db, orders, new ReviewService(db), kds, branchMenu, identity); + + return (db, tables, publicSvc, cafeId, branchId, tableId, itemA, itemB, qrCode); + } + + [Fact] + public async Task ResolveQr_ValidCode_ReturnsBranchInfo() + { + var (_, tables, _, _, branchId, tableId, _, _, qrCode) = CreateFixture(); + var result = await tables.ResolveQrAsync(qrCode); + Assert.NotNull(result); + Assert.Equal(branchId, result!.BranchId); + Assert.Equal(tableId, result.TableId); + Assert.False(result.IsCleaning); + } + + [Fact] + public async Task ResolveQr_InvalidCode_ReturnsNull() + { + var (_, tables, _, _, _, _, _, _, _) = CreateFixture(); + Assert.Null(await tables.ResolveQrAsync("missing_code")); + } + + [Fact] + public async Task GetBranchMenu_ExcludesUnavailableItems() + { + var (db, _, pub, cafeId, branchId, _, itemA, itemB, _) = CreateFixture(); + var menuSvc = new BranchMenuService(db); + await menuSvc.UpsertOverrideAsync( + cafeId, branchId, itemB, + new UpsertBranchMenuOverrideRequest(false, null), + PlanTier.Pro, EmployeeRole.Manager, "u1"); + + var menu = await pub.GetBranchMenuAsync(cafeId, branchId); + var ids = menu!.Categories.SelectMany(c => c.Items).Select(i => i.Id).ToList(); + Assert.Contains(itemA, ids); + Assert.DoesNotContain(itemB, ids); + } + + [Fact] + public async Task GetBranchMenu_AppliesBranchPriceOverride() + { + var (db, _, pub, cafeId, branchId, _, itemA, _, _) = CreateFixture(); + await new BranchMenuService(db).UpsertOverrideAsync( + cafeId, branchId, itemA, + new UpsertBranchMenuOverrideRequest(true, 88_000m), + PlanTier.Pro, EmployeeRole.Manager, "u1"); + + var item = (await pub.GetBranchMenuAsync(cafeId, branchId))! + .Categories.SelectMany(c => c.Items) + .First(i => i.Id == itemA); + Assert.Equal(88_000m, item.Price); + } + + [Fact] + public async Task PlaceGuestOrder_ValidItems_CreatesOrderWithGuestQrSource() + { + var (db, _, pub, cafeId, branchId, tableId, itemA, _, _) = CreateFixture(); + var (data, code, _) = await pub.PlaceBranchGuestOrderAsync( + cafeId, + branchId, + new PlaceGuestOrderRequest(tableId, "Guest", null, [new CreateOrderItemRequest(itemA, 2, null)])); + + Assert.Null(code); + Assert.NotNull(data); + var order = await db.Orders.Include(o => o.Items).FirstAsync(o => o.Id == data!.OrderId); + Assert.Equal(OrderSource.GuestQr, order.Source); + Assert.Equal(2, order.Items.Sum(i => i.Quantity)); + } + + [Fact] + public async Task PlaceGuestOrder_MergesWithExistingOpenOrder() + { + var (db, _, pub, cafeId, branchId, tableId, itemA, _, _) = CreateFixture(); + await pub.PlaceBranchGuestOrderAsync( + cafeId, branchId, + new PlaceGuestOrderRequest(tableId, null, null, [new CreateOrderItemRequest(itemA, 1, null)])); + + var (second, code, _) = await pub.PlaceBranchGuestOrderAsync( + cafeId, branchId, + new PlaceGuestOrderRequest(tableId, null, null, [new CreateOrderItemRequest(itemA, 1, null)])); + + Assert.Null(code); + Assert.Equal(1, await db.Orders.CountAsync(o => o.TableId == tableId)); + var order = await db.Orders.Include(o => o.Items).FirstAsync(o => o.Id == second!.OrderId); + Assert.Equal(2, order.Items.Sum(i => i.Quantity)); + } + + [Fact] + public async Task PlaceGuestOrder_CleaningTable_ReturnsTableCleaning() + { + var (db, _, pub, cafeId, branchId, tableId, itemA, _, _) = CreateFixture(); + var table = await db.Tables.FirstAsync(t => t.Id == tableId); + table.IsCleaning = true; + await db.SaveChangesAsync(); + + var (_, code, _) = await pub.PlaceBranchGuestOrderAsync( + cafeId, branchId, + new PlaceGuestOrderRequest(tableId, null, null, [new CreateOrderItemRequest(itemA, 1, null)])); + + Assert.Equal("TABLE_CLEANING", code); + } + + [Fact] + public async Task PlaceGuestOrder_InvalidMenuItem_ReturnsInvalidMenuItems() + { + var (_, _, pub, cafeId, branchId, tableId, _, _, _) = CreateFixture(); + var (_, code, _) = await pub.PlaceBranchGuestOrderAsync( + cafeId, branchId, + new PlaceGuestOrderRequest(tableId, null, null, [new CreateOrderItemRequest("missing", 1, null)])); + + Assert.Equal("INVALID_MENU_ITEMS", code); + } +} diff --git a/tests/Meezi.API.Tests/ShiftServiceTests.cs b/tests/Meezi.API.Tests/ShiftServiceTests.cs new file mode 100644 index 0000000..e8d6cdf --- /dev/null +++ b/tests/Meezi.API.Tests/ShiftServiceTests.cs @@ -0,0 +1,213 @@ +using Meezi.API.Models.Orders; +using Meezi.API.Services; +using Meezi.Core.Entities; +using Meezi.Core.Enums; +using Meezi.Core.Interfaces; +using Meezi.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Xunit; + +namespace Meezi.API.Tests; + +public class ShiftServiceTests +{ + private static ( + AppDbContext Db, + ShiftService Shifts, + OrderService Orders, + string CafeId, + string BranchId, + string UserId, + string MenuItemId) + CreateFixture() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + var db = new AppDbContext(options); + + var cafeId = "cafe-shift"; + var branchId = "branch-main"; + var userId = "emp-cashier"; + var menuItemId = "item-1"; + var categoryId = "cat-1"; + + db.Cafes.Add(new Cafe { Id = cafeId, Name = "Shift Cafe", Slug = "shift-cafe", PlanTier = PlanTier.Pro }); + db.Branches.Add(new Branch + { + Id = branchId, + CafeId = cafeId, + Name = "Main", + IsActive = true, + UpdatedAt = DateTime.UtcNow + }); + db.Employees.Add(new Employee + { + Id = userId, + CafeId = cafeId, + BranchId = branchId, + Name = "Cashier", + Phone = "09121111111", + Role = EmployeeRole.Cashier + }); + db.MenuCategories.Add(new MenuCategory + { + Id = categoryId, + CafeId = cafeId, + Name = "Menu", + NameEn = "Menu", + SortOrder = 0 + }); + db.MenuItems.Add(new MenuItem + { + Id = menuItemId, + CafeId = cafeId, + CategoryId = categoryId, + Name = "Tea", + NameEn = "Tea", + Price = 100_000m, + IsAvailable = true + }); + db.SaveChanges(); + + var shifts = new ShiftService(db); + var orders = new OrderService(db, new NoOpKdsNotifier(), new NoOpSnappfood(), new NoOpDeliverySync(), shifts, TestServiceScopeFactory.Create(), new NoOpOrderNotificationService(), new NoOpInventoryService(), new NoOpLoyaltyService()); + return (db, shifts, orders, cafeId, branchId, userId, menuItemId); + } + + [Fact] + public async Task OpenShift_Succeeds() + { + var (_, shifts, _, cafeId, branchId, userId, _) = CreateFixture(); + + var result = await shifts.OpenShiftAsync(cafeId, branchId, 500_000m, userId); + + Assert.True(result.Success); + Assert.Equal(ShiftStatus.Open, result.Data!.Status); + Assert.Equal(500_000m, result.Data.OpeningCash); + } + + [Fact] + public async Task OpenShift_WhenAlreadyOpen_ReturnsShiftAlreadyOpen() + { + var (_, shifts, _, cafeId, branchId, userId, _) = CreateFixture(); + + await shifts.OpenShiftAsync(cafeId, branchId, 100m, userId); + var second = await shifts.OpenShiftAsync(cafeId, branchId, 200m, userId); + + Assert.False(second.Success); + Assert.Equal("SHIFT_ALREADY_OPEN", second.ErrorCode); + } + + [Fact] + public async Task CloseShift_CalculatesDiscrepancy_FromCashPaymentsAndWithdrawals() + { + var (_, shifts, _, cafeId, branchId, userId, _) = CreateFixture(); + + var opened = await shifts.OpenShiftAsync(cafeId, branchId, 1_000_000m, userId); + var shiftId = opened.Data!.Id; + + await shifts.RecordTransactionAsync( + cafeId, shiftId, CashTransactionType.OrderPayment, PaymentMethod.Cash, 300_000m, userId); + await shifts.RecordTransactionAsync( + cafeId, shiftId, CashTransactionType.OrderPayment, PaymentMethod.Card, 200_000m, userId); + await shifts.RecordTransactionAsync( + cafeId, shiftId, CashTransactionType.Withdrawal, PaymentMethod.Cash, 50_000m, userId); + + var closed = await shifts.CloseShiftAsync(cafeId, shiftId, 1_300_000m, userId); + + Assert.True(closed.Success); + Assert.Equal(1_250_000m, closed.Data!.ExpectedCash); + Assert.Equal(50_000m, closed.Data.Discrepancy); + Assert.Equal(ShiftStatus.Closed, closed.Data.Status); + } + + [Fact] + public async Task RecordPayments_WithOpenShift_RecordsCashTransactions() + { + var (db, shifts, orders, cafeId, branchId, userId, menuItemId) = CreateFixture(); + await shifts.OpenShiftAsync(cafeId, branchId, 0m, userId); + + var tenant = new TenantContext { CafeId = cafeId, UserId = userId, Role = EmployeeRole.Cashier }; + var created = await orders.CreateOrderAsync( + cafeId, + tenant, + new CreateOrderRequest( + OrderType.DineIn, + branchId, + null, + null, + "Guest", + null, + null, + null, + [new CreateOrderItemRequest(menuItemId, 1, null)])); + Assert.True(created.Success); + + var pay = await orders.RecordPaymentsAsync( + cafeId, + created.Data!.Id, + new RecordPaymentsRequest( + [new CreatePaymentRequest(PaymentMethod.Cash, 100_000m, null), + new CreatePaymentRequest(PaymentMethod.Card, 50_000m, null)]), + userId); + + Assert.True(pay.Success); + + var shift = await db.RegisterShifts.Include(s => s.Transactions) + .FirstAsync(s => s.BranchId == branchId && s.Status == ShiftStatus.Open); + Assert.Equal(2, shift.Transactions.Count); + Assert.Contains(shift.Transactions, t => t.Method == PaymentMethod.Cash && t.Amount == 100_000m); + } + + [Fact] + public async Task RecordPayments_WithoutOpenShift_ReturnsNoOpenShift() + { + var (_, _, orders, cafeId, branchId, userId, menuItemId) = CreateFixture(); + + var tenant = new TenantContext { CafeId = cafeId, UserId = userId }; + var created = await orders.CreateOrderAsync( + cafeId, + tenant, + new CreateOrderRequest( + OrderType.DineIn, + branchId, + null, + null, + "Guest", + null, + null, + null, + [new CreateOrderItemRequest(menuItemId, 1, null)])); + Assert.True(created.Success); + + var pay = await orders.RecordPaymentsAsync( + cafeId, + created.Data!.Id, + new RecordPaymentsRequest([new CreatePaymentRequest(PaymentMethod.Cash, 100_000m, null)]), + userId); + + Assert.False(pay.Success); + Assert.Equal("NO_OPEN_SHIFT", pay.ErrorCode); + } + + private sealed class NoOpKdsNotifier : IKdsNotifier + { + public Task NotifyOrderCreatedAsync(string cafeId, LiveOrderDto order, CancellationToken cancellationToken = default) => + Task.CompletedTask; + public Task NotifyOrderStatusChangedAsync(string cafeId, string orderId, OrderStatus status, CancellationToken cancellationToken = default) => + Task.CompletedTask; + public Task NotifyTableStatusChangedAsync(string cafeId, CancellationToken cancellationToken = default) => + Task.CompletedTask; + } + + private sealed class NoOpSnappfood : ISnappfoodClient + { + public Task AcknowledgeOrderAsync(string snappfoodOrderId, CancellationToken cancellationToken = default) => + Task.CompletedTask; + public Task NotifyOrderDeliveredAsync(string snappfoodOrderId, CancellationToken cancellationToken = default) => + Task.CompletedTask; + public Task NotifyOrderStatusAsync(string snappfoodOrderId, string status, CancellationToken cancellationToken = default) => + Task.CompletedTask; + } +} diff --git a/tests/Meezi.API.Tests/TableContextTests.cs b/tests/Meezi.API.Tests/TableContextTests.cs new file mode 100644 index 0000000..d354227 --- /dev/null +++ b/tests/Meezi.API.Tests/TableContextTests.cs @@ -0,0 +1,19 @@ +using Meezi.Core.Constants; +using Xunit; + +namespace Meezi.API.Tests; + +public class TableContextTests +{ + [Fact] + public void PlanPricing_Pro_HasExpectedToman() + { + Assert.Equal(1_490_000m, PlanPricing.MonthlyToman(Core.Enums.PlanTier.Pro)); + } + + [Fact] + public void PlanPricing_ToRials_ConvertsToman() + { + Assert.Equal(14_900_000L, PlanPricing.ToRials(1_490_000m)); + } +} diff --git a/tests/Meezi.API.Tests/TestServiceScopeFactory.cs b/tests/Meezi.API.Tests/TestServiceScopeFactory.cs new file mode 100644 index 0000000..63111c0 --- /dev/null +++ b/tests/Meezi.API.Tests/TestServiceScopeFactory.cs @@ -0,0 +1,25 @@ +using Meezi.API.Services.Printing; +using Microsoft.Extensions.DependencyInjection; + +namespace Meezi.API.Tests; + +internal sealed class NoOpPrinterService : IPrinterService +{ + public Task PrintReceiptAsync(string cafeId, string orderId, CancellationToken ct = default) => + Task.FromResult(PrintResult.Ok()); + + public Task PrintKitchenTicketAsync(string cafeId, string orderId, CancellationToken ct = default) => + Task.FromResult(PrintResult.Ok()); + + public Task TestPrintAsync(string printerIp, int port, CancellationToken ct = default) => + Task.FromResult(PrintResult.Ok()); +} + +internal static class TestServiceScopeFactory +{ + public static IServiceScopeFactory Create() => + new ServiceCollection() + .AddSingleton() + .BuildServiceProvider() + .GetRequiredService(); +} diff --git a/tests/Meezi.API.Tests/xunit.runner.json b/tests/Meezi.API.Tests/xunit.runner.json new file mode 100644 index 0000000..dd80f43 --- /dev/null +++ b/tests/Meezi.API.Tests/xunit.runner.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", + "parallelizeAssembly": false, + "parallelizeTestCollections": false +} diff --git a/tests/load/README.md b/tests/load/README.md new file mode 100644 index 0000000..f68f3de --- /dev/null +++ b/tests/load/README.md @@ -0,0 +1,29 @@ +# Meezi load tests (k6) + +Smoke-test public endpoints and verify rate limiting under abuse. + +## Prerequisites + +Install [k6](https://k6.io/docs/get-started/installation/). + +## Run + +```powershell +# API must be running (e.g. docker compose or dotnet run) +$env:BASE_URL = "http://localhost:5080" +k6 run tests/load/public-abuse.js +``` + +## Variables + +| Env | Default | Description | +|-----|---------|-------------| +| `BASE_URL` | `http://localhost:5080` | API root | +| `QR_CODE` | `demo_table_01` | QR resolver code | + +## Expected + +- Most requests succeed under normal VUs. +- At high rate, some responses return **429** with `RATE_LIMITED` (ASP.NET or Redis limits). + +See `docs/SECURITY.md` for limit values. diff --git a/tests/load/public-abuse.js b/tests/load/public-abuse.js new file mode 100644 index 0000000..13b46d5 --- /dev/null +++ b/tests/load/public-abuse.js @@ -0,0 +1,40 @@ +import http from "k6/http"; +import { check, sleep } from "k6"; + +const baseUrl = __ENV.BASE_URL || "http://localhost:5080"; +const qrCode = __ENV.QR_CODE || "demo_table_01"; + +export const options = { + stages: [ + { duration: "15s", target: 5 }, + { duration: "30s", target: 20 }, + { duration: "15s", target: 0 }, + ], + thresholds: { + http_req_failed: ["rate<0.15"], + http_req_duration: ["p(95)<3000"], + }, +}; + +export default function () { + const health = http.get(`${baseUrl}/health`); + check(health, { "health ok": (r) => r.status === 200 }); + + const discover = http.get( + `${baseUrl}/api/public/discover?city=${encodeURIComponent("تهران")}&requireProfile=false` + ); + check(discover, { + "discover ok": (r) => r.status === 200, + "discover json": (r) => r.json("success") === true, + }); + + const qr = http.get(`${baseUrl}/api/q/${qrCode}`); + check(qr, { + "qr ok or not found": (r) => r.status === 200 || r.status === 404, + }); + + const security = http.get(`${baseUrl}/api/public/security-config`); + check(security, { "security-config ok": (r) => r.status === 200 }); + + sleep(0.3); +} diff --git a/tools/MenuImageImporter/MenuImageImporter.csproj b/tools/MenuImageImporter/MenuImageImporter.csproj new file mode 100644 index 0000000..2262596 --- /dev/null +++ b/tools/MenuImageImporter/MenuImageImporter.csproj @@ -0,0 +1,11 @@ + + + + Exe + + + + + + + diff --git a/tools/MenuImageImporter/Program.cs b/tools/MenuImageImporter/Program.cs new file mode 100644 index 0000000..f00a598 --- /dev/null +++ b/tools/MenuImageImporter/Program.cs @@ -0,0 +1,73 @@ +using System.Text.Json; +using Meezi.Infrastructure.Data; + +/// +/// One-time importer: copies Food-101 JPEGs into uploads/{cafeId}/ and updates menu-image-manifest paths. +/// Usage: dotnet run --project tools/MenuImageImporter -- --food101 "C:\data\food-101\images" --cafe cafe_demo_001 --out uploads +/// +var argsList = args.ToList(); +string? food101Root = GetArg("--food101"); +var cafeId = GetArg("--cafe") ?? "cafe_demo_001"; +var outRoot = GetArg("--out") ?? "uploads"; +var manifestPath = GetArg("--manifest") + ?? Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "data", "menu-image-manifest.json")); + +var defaultClassMap = DemoMenuCatalog.Items + .ToDictionary(i => i.Id, i => i.Food101Class, StringComparer.Ordinal); + +if (string.IsNullOrEmpty(food101Root) || !Directory.Exists(food101Root)) +{ + Console.WriteLine("Food-101 image root not found. Pass --food101 ."); + Console.WriteLine("Mapping (demo item id → Food-101 class folder):"); + foreach (var (id, cls) in defaultClassMap) + Console.WriteLine($" {id} → {cls}"); + return 1; +} + +if (!File.Exists(manifestPath)) +{ + Console.WriteLine($"Manifest not found: {manifestPath}"); + return 1; +} + +var manifestJson = await File.ReadAllTextAsync(manifestPath); +using var doc = JsonDocument.Parse(manifestJson); +var root = doc.RootElement.Clone(); +var items = root.GetProperty("items"); + +var cafeDir = Path.Combine(outRoot, cafeId); +Directory.CreateDirectory(cafeDir); + +var updated = 0; +foreach (var (itemId, className) in defaultClassMap) +{ + var classDir = Path.Combine(food101Root, className); + if (!Directory.Exists(classDir)) + { + Console.WriteLine($"Skip {itemId}: class folder missing {className}"); + continue; + } + + var source = Directory.GetFiles(classDir, "*.jpg").Concat(Directory.GetFiles(classDir, "*.jpeg")).FirstOrDefault(); + if (source is null) continue; + + var destName = $"{itemId}.jpg"; + var destPath = Path.Combine(cafeDir, destName); + File.Copy(source, destPath, overwrite: true); + + if (items.TryGetProperty(itemId, out var entry) && entry.ValueKind == JsonValueKind.Object) + { + var rel = $"/uploads/{cafeId}/{destName}"; + Console.WriteLine($"Copied {itemId} → {destPath} (set imageUrl: {rel})"); + updated++; + } +} + +Console.WriteLine($"Done. {updated} images copied to {cafeDir}. Re-run API seeder or PATCH menu items with /uploads paths."); +return 0; + +string? GetArg(string name) +{ + var i = argsList.IndexOf(name); + return i >= 0 && i + 1 < argsList.Count ? argsList[i + 1] : null; +} diff --git a/tools/SyncMenuManifest/Program.cs b/tools/SyncMenuManifest/Program.cs new file mode 100644 index 0000000..e8bc297 --- /dev/null +++ b/tools/SyncMenuManifest/Program.cs @@ -0,0 +1,73 @@ +using System.Text.Json; +using Meezi.Infrastructure.Data; + +static string FindRepoRoot() +{ + var dir = new DirectoryInfo(AppContext.BaseDirectory); + while (dir is not null) + { + if (File.Exists(Path.Combine(dir.FullName, "MEEZI_PRD.md")) + || Directory.Exists(Path.Combine(dir.FullName, "data"))) + return dir.FullName; + dir = dir.Parent; + } + + return Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..")); +} + +var repoRoot = FindRepoRoot(); +var manifestPath = Path.Combine(repoRoot, "data", "menu-image-manifest.json"); +var demoMenuPath = Path.Combine(repoRoot, "data", "demo-menu-food101.json"); + +var items = new Dictionary(StringComparer.Ordinal); +foreach (var item in DemoMenuCatalog.Items) +{ + items[item.Id] = new + { + food101Class = item.Food101Class, + imageUrl = Food101ImageFallbacks.Resolve( + item.Food101Class, + MenuItemImageDefaults.InferKind(item.CategoryId)), + nameEn = item.NameEn, + categoryId = item.CategoryId + }; +} + +var manifest = new +{ + version = 2, + source = "Food-101 class mapping (Unsplash fallbacks until Kaggle JPEG import)", + defaults = new + { + drink = MenuImageManifest.GetDefaultDrinkImageUrl(), + food = MenuImageManifest.GetDefaultFoodImageUrl() + }, + items +}; + +var options = new JsonSerializerOptions { WriteIndented = true }; +await File.WriteAllTextAsync(manifestPath, JsonSerializer.Serialize(manifest, options) + Environment.NewLine); + +var demoExport = new +{ + version = 1, + cafeId = "cafe_demo_001", + categories = DemoMenuCatalog.Categories, + items = DemoMenuCatalog.Items.Select(i => new + { + i.Id, + i.CategoryId, + i.Name, + i.NameEn, + i.NameAr, + i.Description, + priceToman = i.PriceToman, + i.DiscountPercent, + i.Food101Class, + imageUrl = DemoMenuCatalog.ResolveItemImageUrl(i) + }) +}; +await File.WriteAllTextAsync(demoMenuPath, JsonSerializer.Serialize(demoExport, options) + Environment.NewLine); + +Console.WriteLine($"Wrote {items.Count} items → {manifestPath}"); +Console.WriteLine($"Wrote demo menu export → {demoMenuPath}"); diff --git a/tools/SyncMenuManifest/SyncMenuManifest.csproj b/tools/SyncMenuManifest/SyncMenuManifest.csproj new file mode 100644 index 0000000..4770ee3 --- /dev/null +++ b/tools/SyncMenuManifest/SyncMenuManifest.csproj @@ -0,0 +1,11 @@ + + + Exe + net10.0 + enable + enable + + + + +