feat(web-client-tpos): implement POS order creation via BFF API
- Add POST /api/bff/pos/orders endpoint (Dapper, order+items+payment_method) - Add CreatePosOrderAsync to PosDataService with camelCase JSON serialization - Wire CafeDesktop ConfirmPayment to API (async, ProductId tracking, fallback) - Add TryRestoreSessionAsync to POS page init for JWT auth - Replace Forbid() with Unauthorized() + diagnostic logging
This commit is contained in:
53
.scratchpad/context.md
Normal file
53
.scratchpad/context.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Scratchpad Context — GoodGo Platform
|
||||
> Updated: 2026-03-03 14:22
|
||||
|
||||
## Trạng thái hệ thống
|
||||
|
||||
### Infrastructure
|
||||
- **Docker**: 29 containers running (healthy), Traefik reverse proxy
|
||||
- **DB**: PostgreSQL (postgres-local), separate DB per service
|
||||
- **Cache**: Redis (redis-local)
|
||||
- **Queue**: RabbitMQ (rabbitmq-local)
|
||||
- **Storage**: MinIO (minio-local)
|
||||
|
||||
### Services (26 .NET microservices)
|
||||
| Service | Status | Notes |
|
||||
|---------|--------|-------|
|
||||
| iam-service-net | ✅ healthy | Auth, Users, Roles |
|
||||
| merchant-service-net | ✅ healthy | Merchants, Shops, Staff |
|
||||
| catalog-service-net | ✅ healthy | Products, Categories |
|
||||
| order-service-net | ✅ healthy | Orders, Order Items |
|
||||
| booking-service-net | ✅ healthy | Appointments, Resources |
|
||||
| fnb-engine-net | ✅ healthy | Tables, Sessions |
|
||||
| inventory-service-net | ✅ healthy | Stock management |
|
||||
| promotion-service-net | ✅ healthy | Campaigns, Vouchers |
|
||||
| membership-service-net | ✅ healthy | Members, Levels |
|
||||
| wallet-service-net | ✅ healthy | Wallets, Transactions |
|
||||
| chat-service-net | ✅ healthy | Chat, SignalR |
|
||||
| social-service-net | ✅ healthy | Social features |
|
||||
| storage-service-net | ✅ healthy | File storage |
|
||||
| mining-service-net | ✅ healthy | Data mining |
|
||||
| mission-service-net | ✅ healthy | Gamification |
|
||||
| ads-*-service-net (5) | ✅ healthy | Ads ecosystem |
|
||||
| mkt-*-service-net (4) | ✅ healthy | Marketing integrations |
|
||||
|
||||
### Apps
|
||||
| App | Type | Port | Status |
|
||||
|-----|------|------|--------|
|
||||
| web-client-tpos-net | Blazor WASM POS | 3001 | ✅ Running |
|
||||
| web-client-base-net | Blazor Admin | — | Not deployed locally |
|
||||
| app-client-base-net | .NET MAUI Mobile | — | Not deployed |
|
||||
| app-client-base-swift | Swift iOS | — | Not deployed |
|
||||
| web-docs | Documentation site | — | Not deployed |
|
||||
|
||||
## Recent Changes (từ Git log)
|
||||
1. `d969f3d` — **feat**: Replace hardcoded POS data with API-driven endpoints
|
||||
2. `fe6e14c` — **feat**: Unify POS with inline payment and tabs (path fix)
|
||||
3. `15404a8` — **feat**: Unify POS page with tabs and inline payment
|
||||
4. `617a7ca` — **fix**: Resolve 500 error on GET /api/v1/users endpoint
|
||||
5. `ad6fe03` — **feat**: Add users management page and enhance roles CRUD
|
||||
6. `f3bfcc8` — **fix**: Roles API response parsing and property mapping
|
||||
7. `f353f88` — **feat**: Redesign Home/NotFound pages, localize Dashboard
|
||||
|
||||
## Blockers
|
||||
- Chưa có
|
||||
40
.scratchpad/plan.md
Normal file
40
.scratchpad/plan.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# Task Plan — GoodGo Platform
|
||||
> Updated: 2026-03-03 14:22
|
||||
|
||||
## ✅ Completed Tasks
|
||||
1. ~~Admin Users & Roles — CRUD, phân quyền~~
|
||||
2. ~~POS Unified page — 3 tabs (Sale, History, Dashboard)~~
|
||||
3. ~~POS Inline Payment Flow~~
|
||||
4. ~~Replace hardcoded POS data with API endpoints~~
|
||||
|
||||
## Pending Tasks (Ưu tiên cao → thấp)
|
||||
|
||||
### 🔴 P1 — POS Core (Hoàn thiện)
|
||||
- [ ] **Order API Create**: `ConfirmPayment` gọi Order Service API tạo order thật (hiện chỉ lưu in-memory session)
|
||||
- [ ] **Order Items**: Lưu order_items (product_id, qty, unit_price) khi confirm
|
||||
- [ ] **Payment Method column**: Add `payment_method` column vào orders table nếu chưa có
|
||||
- [ ] **Print receipt**: Implement in hóa đơn (hoặc generate PDF)
|
||||
|
||||
### 🟡 P2 — Dashboard Real Data
|
||||
- [ ] **Hourly chart**: Verify `order_items` + `payment_method` tables exist
|
||||
- [ ] **Popular items**: Test with real order data
|
||||
- [ ] **History search/filter**: Implement date range filter (today/7 days/30 days)
|
||||
|
||||
### 🟡 P3 — Admin Panel (web-client-tpos-net)
|
||||
- [ ] **Products CRUD**: Thêm/sửa/xóa products từ Admin
|
||||
- [ ] **Inventory management**: Stock alerts, reorder
|
||||
- [ ] **Staff management**: Assign staff to shops
|
||||
- [ ] **Promotions**: Campaign management UI
|
||||
|
||||
### 🟢 P4 — Frontend Apps
|
||||
- [ ] **web-client-base-net**: Deploy locally, kiểm tra
|
||||
- [ ] **app-client-base-swift**: iOS app review
|
||||
- [ ] **web-docs**: Documentation site
|
||||
|
||||
### 🟢 P5 — Infrastructure
|
||||
- [ ] **Observability**: Prometheus + Grafana
|
||||
- [ ] **CI/CD**: GitHub Actions
|
||||
- [ ] **Testing**: Unit + Integration tests coverage
|
||||
|
||||
## Next Task
|
||||
> **P1: Order API Create** — Implement `POST /api/bff/orders` endpoint + call from POS ConfirmPayment
|
||||
@@ -8,6 +8,7 @@
|
||||
@layout PosLayout
|
||||
@inherits PosBase
|
||||
@using WebClientTpos.Client.Services
|
||||
@inject AuthService AuthService
|
||||
@inject PosDataService DataService
|
||||
|
||||
@* ═══════════════ MAIN CONTENT AREA ═══════════════ *@
|
||||
@@ -463,6 +464,10 @@
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
// EN: Restore auth session from localStorage so API calls have JWT token
|
||||
// VI: Khôi phục session auth từ localStorage để API calls có JWT token
|
||||
await AuthService.TryRestoreSessionAsync();
|
||||
|
||||
try
|
||||
{
|
||||
var productsTask = DataService.GetProductsAsync(ShopId);
|
||||
@@ -472,7 +477,7 @@
|
||||
var apiProducts = await productsTask;
|
||||
var apiCategories = await categoriesTask;
|
||||
|
||||
_products = apiProducts.Select(p => new Product(p.Name, p.Price, p.Category ?? "Khác")).ToList();
|
||||
_products = apiProducts.Select(p => new Product(p.Id, p.Name, p.Price, p.Category ?? "Khác")).ToList();
|
||||
|
||||
var catNames = apiCategories.Select(c => c.Name).ToList();
|
||||
if (catNames.Count > 0)
|
||||
@@ -490,9 +495,9 @@
|
||||
private void AddToCart(Product product)
|
||||
{
|
||||
if (_paymentStep != PayStep.None) return;
|
||||
var existing = _cartItems.FirstOrDefault(i => i.Name == product.Name);
|
||||
var existing = _cartItems.FirstOrDefault(i => i.ProductId == product.Id);
|
||||
if (existing != null) existing.Qty++;
|
||||
else _cartItems.Add(new CartItem(product.Name, product.Price));
|
||||
else _cartItems.Add(new CartItem(product.Id, product.Name, product.Price));
|
||||
}
|
||||
|
||||
private void ChangeQty(CartItem item, int delta)
|
||||
@@ -567,14 +572,39 @@
|
||||
_receivedAmount = val;
|
||||
}
|
||||
|
||||
private void ConfirmPayment()
|
||||
private bool _paymentProcessing;
|
||||
|
||||
private async Task ConfirmPayment()
|
||||
{
|
||||
if (_paymentProcessing) return;
|
||||
_paymentProcessing = true;
|
||||
StateHasChanged();
|
||||
|
||||
_lastOrderTotal = CartTotal;
|
||||
_lastTransactionId = $"POS-{DateTime.Now:yyyyMMdd}-{DateTime.Now:HHmmss}";
|
||||
var methodLabel = _selectedMethod switch { "cash" => "Tiền mặt", "card" => "Thẻ", "qr" => "QR Code", _ => "Chuyển khoản" };
|
||||
|
||||
// EN: Call API to create real order in DB
|
||||
// VI: Gọi API tạo đơn hàng thật trong DB
|
||||
try
|
||||
{
|
||||
var orderReq = new PosDataService.CreatePosOrderRequest(
|
||||
ShopId,
|
||||
_selectedMethod,
|
||||
_cartItems.Select(i => new PosDataService.PosOrderItemRequest(
|
||||
i.ProductId, i.Name, i.Qty, i.Price)).ToList());
|
||||
|
||||
var result = await DataService.CreatePosOrderAsync(orderReq);
|
||||
_lastTransactionId = result?.TransactionId ?? $"POS-{DateTime.Now:yyyyMMdd}-{DateTime.Now:HHmmss}";
|
||||
}
|
||||
catch
|
||||
{
|
||||
// EN: Fallback — generate local ID if API fails
|
||||
// VI: Fallback — tạo ID local nếu API lỗi
|
||||
_lastTransactionId = $"POS-{DateTime.Now:yyyyMMdd}-{DateTime.Now:HHmmss}";
|
||||
}
|
||||
|
||||
// EN: Save to session history (in-memory for this POS session)
|
||||
// VI: Lưu vào lịch sử phiên (in-memory cho phiên POS hiện tại)
|
||||
var methodLabel = _selectedMethod switch { "cash" => "Tiền mặt", "card" => "Thẻ", "qr" => "QR Code", _ => "Chuyển khoản" };
|
||||
_sessionOrders.Insert(0, new SessionOrder(
|
||||
_lastTransactionId,
|
||||
string.Join(", ", _cartItems.Select(i => $"{i.Name} x{i.Qty}")),
|
||||
@@ -584,11 +614,14 @@
|
||||
"Hoàn thành"
|
||||
));
|
||||
|
||||
// EN: Invalidate dashboard cache so next view refreshes
|
||||
// VI: Xóa cache dashboard để refresh khi xem lại
|
||||
// EN: Invalidate caches so next view refreshes
|
||||
// VI: Xóa cache để refresh khi xem lại
|
||||
_dashLoaded = false;
|
||||
_historyLoaded = false;
|
||||
|
||||
_paymentProcessing = false;
|
||||
_paymentStep = PayStep.Success;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void ResetAfterPayment()
|
||||
@@ -691,9 +724,10 @@
|
||||
}
|
||||
|
||||
// ═══════════════ RECORDS ═══════════════
|
||||
private record Product(string Name, decimal Price, string Category);
|
||||
private class CartItem(string name, decimal price)
|
||||
private record Product(Guid Id, string Name, decimal Price, string Category);
|
||||
private class CartItem(Guid productId, string name, decimal price)
|
||||
{
|
||||
public Guid ProductId { get; set; } = productId;
|
||||
public string Name { get; set; } = name;
|
||||
public decimal Price { get; set; } = price;
|
||||
public int Qty { get; set; } = 1;
|
||||
|
||||
@@ -242,4 +242,28 @@ public class PosDataService
|
||||
$"api/bff/pos/dashboard?shopId={shopId}", _jsonOptions)
|
||||
?? new(0, 0, 0, 0, new(), new(), new(), new());
|
||||
}
|
||||
|
||||
// ═══ POS ORDER CREATE ═══
|
||||
|
||||
// EN: POS order creation DTOs
|
||||
// VI: DTOs cho tạo đơn POS
|
||||
public record CreatePosOrderRequest(Guid ShopId, string? PaymentMethod, List<PosOrderItemRequest> Items);
|
||||
public record PosOrderItemRequest(Guid ProductId, string ProductName, int Quantity, decimal UnitPrice);
|
||||
public record CreatePosOrderResponse(Guid OrderId, string TransactionId, decimal TotalAmount, string Status);
|
||||
|
||||
public async Task<CreatePosOrderResponse?> CreatePosOrderAsync(CreatePosOrderRequest req)
|
||||
{
|
||||
AttachToken();
|
||||
// EN: Use camelCase for POST body (ASP.NET model binding default)
|
||||
// VI: Dùng camelCase cho POST body (ASP.NET model binding mặc định)
|
||||
var postOptions = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
var resp = await _http.PostAsJsonAsync("api/bff/pos/orders", req, postOptions);
|
||||
if (resp.IsSuccessStatusCode)
|
||||
return await resp.Content.ReadFromJsonAsync<CreatePosOrderResponse>(_jsonOptions);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -898,7 +898,97 @@ public class BffDataController : ControllerBase
|
||||
_ => method
|
||||
};
|
||||
|
||||
// ═══ POS ORDER CREATE (create real order from POS checkout) ═══
|
||||
|
||||
/// <summary>
|
||||
/// EN: Create a POS order — inserts order + order_items, marks as Paid+Completed.
|
||||
/// VI: Tạo đơn POS — insert order + order_items, đánh dấu Đã thanh toán + Hoàn thành.
|
||||
/// </summary>
|
||||
[HttpPost("pos/orders")]
|
||||
public async Task<IActionResult> CreatePosOrder([FromBody] CreatePosOrderRequest req)
|
||||
{
|
||||
var merchantId = await GetCurrentMerchantIdAsync();
|
||||
if (merchantId == null)
|
||||
{
|
||||
var userId = GetUserIdFromToken();
|
||||
Console.Error.WriteLine($"[BFF] CreatePosOrder: merchantId null. userId={userId}, hasAuth={Request.Headers.ContainsKey("Authorization")}");
|
||||
return Unauthorized(new { message = "Merchant not found", userId });
|
||||
}
|
||||
|
||||
// EN: Verify shop ownership / VI: Xác nhận quyền sở hữu shop
|
||||
var myShopIds = await GetMyShopIdsAsync(merchantId.Value);
|
||||
if (!myShopIds.Contains(req.ShopId))
|
||||
{
|
||||
Console.Error.WriteLine($"[BFF] CreatePosOrder: shop {req.ShopId} not owned by merchant {merchantId}. Owned: [{string.Join(", ", myShopIds)}]");
|
||||
return Unauthorized(new { message = "Shop not owned by current merchant" });
|
||||
}
|
||||
|
||||
var orderId = Guid.NewGuid();
|
||||
var now = DateTime.UtcNow;
|
||||
var totalAmount = req.Items.Sum(i => i.Quantity * i.UnitPrice);
|
||||
var transactionId = $"POS-{now:yyyyMMdd}-{orderId.ToString()[..8].ToUpper()}";
|
||||
|
||||
try
|
||||
{
|
||||
await using var conn = new NpgsqlConnection(ConnStr("order_service"));
|
||||
await conn.OpenAsync();
|
||||
await using var tx = await conn.BeginTransactionAsync();
|
||||
|
||||
// EN: Step 1 — Insert order (status_id=5 = Completed for POS instant payment)
|
||||
// VI: Bước 1 — Insert order (status_id=5 = Hoàn thành cho thanh toán POS tức thì)
|
||||
await conn.ExecuteAsync(
|
||||
@"INSERT INTO orders (id, shop_id, status_id, total_amount, customer_id, notes, payment_method, created_at, updated_at)
|
||||
VALUES (@Id, @ShopId, 5, @Total, @CustomerId, @Notes, @PaymentMethod, @Now, @Now)",
|
||||
new
|
||||
{
|
||||
Id = orderId,
|
||||
req.ShopId,
|
||||
Total = totalAmount,
|
||||
CustomerId = (Guid?)null,
|
||||
Notes = $"POS Order | {transactionId}",
|
||||
PaymentMethod = req.PaymentMethod ?? "cash",
|
||||
Now = now
|
||||
}, tx);
|
||||
|
||||
// EN: Step 2 — Insert order items
|
||||
// VI: Bước 2 — Insert các mục đơn hàng
|
||||
foreach (var item in req.Items)
|
||||
{
|
||||
await conn.ExecuteAsync(
|
||||
@"INSERT INTO order_items (id, order_id, product_id, product_name, product_type, quantity, unit_price, status)
|
||||
VALUES (@Id, @OrderId, @ProductId, @ProductName, 'PreparedFood', @Quantity, @UnitPrice, 'Completed')",
|
||||
new
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
OrderId = orderId,
|
||||
item.ProductId,
|
||||
item.ProductName,
|
||||
item.Quantity,
|
||||
item.UnitPrice
|
||||
}, tx);
|
||||
}
|
||||
|
||||
await tx.CommitAsync();
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
orderId,
|
||||
transactionId,
|
||||
totalAmount,
|
||||
status = "Completed",
|
||||
createdAt = now
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"[BFF] CreatePosOrder error: {ex.Message}");
|
||||
return StatusCode(500, new { message = "Failed to create order", error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
// EN: Request DTOs / VI: DTO yêu cầu
|
||||
public record CreateProductRequest(Guid ShopId, string Name, string? Description, decimal Price, string? Type, string? Sku, string? ImageUrl);
|
||||
public record CreateStaffRequest(Guid MerchantId, string? EmployeeCode, string? Phone, string? Email, string? Role);
|
||||
public record CreatePosOrderRequest(Guid ShopId, string? PaymentMethod, List<PosOrderItemRequest> Items);
|
||||
public record PosOrderItemRequest(Guid ProductId, string ProductName, int Quantity, decimal UnitPrice);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user