feat(api): .NET 10 multi-tenant REST API

Full backend implementation:
- Multi-tenant cafe/restaurant management (menus, orders, tables, staff)
- POS order flow with ZarinPal and Snappfood payment integration
- OTP authentication via Kavenegar SMS
- QR digital menu with public discover/finder endpoints
- Customer loyalty, coupons, CRM
- PostgreSQL via EF Core, Redis for caching/sessions
- Background jobs, webhook handlers
- Full migration history

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-05-27 21:33:48 +03:30
parent 03376b3ea1
commit ef15fd6247
472 changed files with 120358 additions and 0 deletions
@@ -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<IActionResult> 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<IActionResult> 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<IActionResult> 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<PrintJobResultDto>(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<PrintJobResultDto>(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."
};
}