FlowerShop IoT Platform — Epic Features Guide
What each epic delivers, why it matters, and how to use the new functionality via the API and portal UIs.
Last updated: 2026-04-06
EP-01 · Database Foundation & Migrations
Value
Establishes the schema for every customer-facing feature that follows. Without these tables, carts, orders, subscriptions, favourites, notifications, and saved addresses cannot be persisted.
What was delivered
| Table | Purpose |
|---|---|
| Carts / CartItems | DB-backed shopping cart that persists across devices |
| Orders / OrderLines | Multi-line orders with delivery info and timeline |
| Subscriptions | Customer notification preferences (flower type, price, radius) |
| Favourites | Saved bouquets for quick access |
| Notifications | In-app notification feed |
| DeviceRegistrations | Push notification token storage (iOS / Android) |
| SavedAddresses | Customer delivery addresses (max 10) |
| Bouquet.QrShortCode | 8-char alphanumeric code for QR-based bouquet lookup |
| Vendor OpeningHours / Description | Extended vendor profile data |
How to verify
# Check all tables exist in dev database
psql -h localhost -U postgres -d flowershop_iot_dev \
-c "\dt" | grep -E "carts|cart_items|orders|order_lines|subscriptions|favourites|notifications|device_registrations|saved_addresses"
EP-02 · Customer Authentication & Registration
Value
Enables customers to create accounts, receive JWTs with a customer_id claim, and access protected endpoints (cart, orders, profile, favourites).
API usage
Register a new customer:
POST /api/v1/customers/register
Content-Type: application/json
{
"username": "jane",
"email": "jane@example.com",
"password": "SecurePass123!",
"customerName": "Jane Doe",
"preferredVendorType": "FlowerShop"
}
Returns a JWT that the mobile app or web client stores and sends as Authorization: Bearer <token> on every subsequent request.
Dev-mode token (no Keycloak):
POST /api/v1/auth/dev-token
{ "username": "testcustomer", "role": "User" }
EP-03 · Bouquet Discovery, QR & Vendor Profile
Value
Powers the core customer experience — finding nearby bouquets on a map, scanning QR codes on physical vases, and seeing whether a vendor is currently open.
API usage
Browse nearby bouquets (geo-spatial):
GET /api/v1/bouquets/nearby?lat=52.23&lng=21.01&radiusKm=5&sortBy=distance&page=1&pageSize=20
Returns bouquets within 5 km, sorted by distance. Each item includes distanceKm, isFavourite (if authenticated), freshnessScore, and price. Freelance florist coordinates are fuzzed for privacy.
Apply filters:
GET /api/v1/bouquets/nearby?lat=52.23&lng=21.01&radiusKm=10&vendorType=FlowerShop&flowerType=roses&maxPrice=80&search=birthday
Scan a QR code:
GET /api/v1/bouquets/qr/A7xK2m9P
Returns the full bouquet detail. Returns 410 Gone if the bouquet has been sold or expired.
View vendor profile:
GET /api/v1/vendors/{vendorId}/public
Response includes isCurrentlyOpen (computed from the vendor’s structured opening hours), openingHours, description, and activeBouquetCount.
Customer App UI
The Home page (http://localhost:5003/) displays a Leaflet.js interactive map centered on Warsaw. Markers appear for nearby bouquets; clicking one shows name, price, and a link to the detail page.
EP-04 · Order Lifecycle & Stripe
Value
Completes the purchase flow end-to-end: cart → preview → order → Stripe payment → delivery. Adds automatic reservation expiry, idempotent order creation, refunds on cancellation, and delivery QR codes.
API usage — full purchase flow
Step 1 — Add to cart:
POST /api/v1/cart/items
Authorization: Bearer <customer-token>
{ "bouquetId": "..." }
Returns 409 Conflict with vendor-mismatch if the bouquet belongs to a different vendor than existing cart items.
Step 2 — Preview order:
POST /api/v1/orders/preview
{
"bouquetIds": ["id1", "id2"],
"deliveryMethod": "delivery",
"street": "ul. Marszałkowska 1",
"city": "Warszawa",
"postalCode": "00-001"
}
Returns linesSubtotal, deliveryFee (15.00 PLN default for delivery), and totalAmount.
Step 3 — Create order:
POST /api/v1/orders
Idempotency-Key: unique-client-key-123
{
"bouquetIds": ["id1", "id2"],
"deliveryMethod": "delivery",
"expectedTotalAmount": 145.00,
"street": "ul. Marszałkowska 1",
"city": "Warszawa",
"postalCode": "00-001"
}
Returns orderId, paymentClientSecret, stripeEphemeralKey, and stripeCustomerId. Bouquets are reserved for 15 minutes.
Step 4 — Confirm payment (after Stripe client-side):
POST /api/v1/orders/{orderId}/confirm-payment
{ "paymentIntentId": "pi_xxx", "stripeCustomerId": "cus_xxx" }
Step 5 — Cancel (with automatic refund if paid):
POST /api/v1/orders/{orderId}/cancel
{ "reason": "Changed my mind" }
If the order was already paid, a Stripe refund is automatically initiated.
Stripe webhook
The platform listens at POST /api/stripe/webhook for:
payment_intent.succeeded→ confirms payment automaticallypayment_intent.payment_failed→ cancels the order
Idempotency
Any POST /api/v1/orders with the same Idempotency-Key header within 24 hours returns the cached response instead of creating a duplicate order.
Reservation expiry
A background service runs every 30 seconds. Orders stuck in Created status past their 15-minute reservation window are automatically cancelled, and bouquets become available again (with a SignalR push to connected clients).
Delivery QR code
Once an order reaches Paid status, GET /api/v1/orders/{id} includes a deliveryQrCode field — a base64 PNG data URI the customer can show at pickup/delivery.
EP-05 · Vendor Order Management
Value
Gives vendors full visibility and control over incoming orders — view, filter, advance status, and mark delivered.
API usage
List vendor orders:
GET /api/v1/vendor/orders?status=Paid&page=1&pageSize=20
Authorization: Bearer <vendor-token>
View order detail:
GET /api/v1/vendor/orders/{orderId}
Advance order status:
PUT /api/v1/vendor/orders/{orderId}/status
{
"status": "InPreparation",
"estimatedDeliveryAt": "2026-04-06T16:00:00Z"
}
Valid status transitions (vendor-driven):
Paid → InPreparation → ReadyForPickup → InDelivery → Delivered
Any active status → Cancelled
When an order reaches Delivered, all bouquets in the order are automatically marked as Sold.
Vendor Portal UI
Navigate to Orders in the Vendor Portal sidebar (http://localhost:5002/Orders). The page shows tabs for Pending / Confirmed / Ready / Completed. Click an order to see line items and a timeline. Use the status buttons to advance the order through its lifecycle.
EP-06 · Favourites, Subscriptions & Notifications
Value
Lets customers save bouquets for later, set up alerts for new bouquets matching their preferences, and receive push notifications.
API usage
Favourites:
POST /api/v1/favourites/{bouquetId} # Add (idempotent)
DELETE /api/v1/favourites/{bouquetId} # Remove (idempotent)
GET /api/v1/favourites?page=1&pageSize=20
Subscriptions — get notified when matching bouquets appear:
POST /api/v1/subscriptions
{
"type": "ByFlowerType",
"flowerType": "roses",
"maxPrice": 100,
"frequency": "instant",
"centerLatitude": 52.23,
"centerLongitude": 21.01,
"radiusKm": 10
}
Frequency options: instant (push immediately), daily, weekly.
Disable all subscriptions at once:
PUT /api/v1/subscriptions/disable-all
Register device for push notifications:
POST /api/v1/notifications/devices
{ "deviceToken": "fcm-token-xxx", "platform": "Android", "appVersion": "1.2.0" }
Notification feed:
GET /api/v1/notifications?unreadOnly=true&page=1&pageSize=20
PUT /api/v1/notifications/{id}/read
PUT /api/v1/notifications/read-all
Customer App UI
- Favourites page at
/Favourites— card grid of saved bouquets with availability badges. - Subscriptions page at
/Subscriptions— list existing subscriptions and create new ones with a form sidebar. - Heart toggle buttons appear on bouquet cards throughout the app.
EP-07 · Customer Profile & Saved Addresses
Value
Full customer self-service: edit profile, manage delivery addresses, configure notification preferences, and initiate GDPR-compliant account deletion.
API usage
Get profile with stats:
GET /api/v1/profile
Returns name, email, phone, preferred flower types, notification settings, plus aggregated stats (total orders, active subscriptions, total spent).
Update profile:
PUT /api/v1/profile
{ "firstName": "Jane", "lastName": "Smith", "phone": "+48123456789" }
Saved addresses (max 10):
POST /api/v1/profile/addresses
{ "street": "ul. Nowy Świat 15", "city": "Warszawa", "postalCode": "00-029", "isDefault": true }
GET /api/v1/profile/addresses
DELETE /api/v1/profile/addresses/{id}
Request account deletion (30-day grace period):
DELETE /api/v1/profile
{ "confirmationText": "DELETE" }
Returns 202 Accepted. The account enters PendingDeletion status with a 30-day countdown.
Customer App UI
- Profile page at
/Profile— edit name, view order stats, manage notification preferences. - Addresses sub-page at
/Profile/Addresses— list, add, and delete saved addresses.
EP-08 · Real-time Events & Freshness Engine
Value
Powers the live map experience and ensures bouquet freshness scores stay accurate. Customers see bouquets appear/disappear in real-time. Vendors get instant vase status alerts.
How it works
Freshness scoring runs every hour (plus on each MQTT sensor reading):
Score = 100
− (temperature − 25°C) × 3 (if temp > 25°C)
− (40% − humidity) × 1.5 (if humidity < 40%)
− hours_since_placed × 0.5
Labels: "Just picked" (90+), "Very fresh" (70-89), "Fresh" (50-69),
"Good" (30-49), "Last chance" (<30)
Bouquets that drop below score 20 are automatically expired.
SignalR real-time events (connect to /hubs/bouquet):
// Browser — join a location group
connection.invoke("JoinRadiusGroup", 52.23, 21.01, 5.0);
// Listen for events
connection.on("BouquetBecameAvailable", (data) => {
// Add marker to map
});
connection.on("BouquetBecameUnavailable", (data) => {
// Remove marker from map
});
connection.on("FreshnessUpdated", (data) => {
// Update freshness badge on bouquet card
});
connection.on("OrderStatusChanged", (data) => {
// Update order tracking UI
});
Domain events wired to SignalR:
- New bouquet activated →
BouquetBecameAvailablebroadcast to radius groups - Bouquet sold →
BouquetBecameUnavailablebroadcast - Vase goes offline →
VaseStatusChangedto vendor group - Freshness score changes →
FreshnessUpdatedto radius groups
EP-09 · Performance, Reliability & Cross-cutting
Value
Production-hardening: rate limiting prevents abuse, ETag caching reduces bandwidth, the global exception handler ensures consistent error responses, and subscription matching triggers instant push notifications when matching bouquets appear.
Middleware pipeline
| Middleware | Header / Behaviour |
|---|---|
| GlobalExceptionMiddleware | Returns RFC 7807 Problem Details JSON for all unhandled exceptions |
| RequestIdMiddleware | Sets X-Request-Id response header (correlates with logs) |
| LocalisationMiddleware | Reads Accept-Language, defaults to pl-PL |
| ETagMiddleware | Computes SHA-256 ETag on GET responses; returns 304 Not Modified when If-None-Match matches |
| IdempotencyMiddleware | Caches POST responses by Idempotency-Key header for 24 hours |
Rate limiting policies
| Policy | Limit | Applied to |
|---|---|---|
fixed (global) |
100 req/min | All endpoints |
auth |
10 req/min | Login, registration |
orders |
20 req/min (sliding) | Order creation |
Exceeded limits return 429 Too Many Requests with a Retry-After header.
Subscription matching engine
A background service listens for BouquetBecameAvailable domain events. For each new bouquet, it finds customers with matching active subscriptions (by flower type, vendor, price range, radius). Subscriptions with frequency: instant trigger an immediate push notification.
EP-10 · Analytics API
Value
Fixes the critical VendorId.New() bug (all analytics queries were returning data for random vendors) and delivers 6 working analytics endpoints covering vendor performance, vase health, real-time dashboards, platform-wide insights, peer benchmarks, and CSV export.
API usage
Vendor dashboard analytics:
GET /api/v1/analytics/vendor?period=30
Authorization: Bearer <vendor-token>
Returns: total revenue, order count, average order value, top bouquets by revenue, and daily trend data.
Vase performance:
GET /api/v1/analytics/vases?vaseId={id}&period=7
Returns: uptime percentage, scan count, orders attributed, average freshness, environment metrics (temp, humidity).
Real-time snapshot:
GET /api/v1/analytics/realtime
Returns: active vases (online count), available bouquets, open orders, hourly revenue.
Platform insights (admin only):
GET /api/v1/analytics/platform?period=30
Authorization: Bearer <admin-token>
Returns: total vendors, customers, GMV (gross marketplace volume), top vendors by revenue.
Peer benchmarks:
GET /api/v1/analytics/benchmarks?period=30
Returns: revenue percentile vs. peers, performance ratios, strengths and improvement areas.
Export to CSV:
GET /api/v1/analytics/export?exportType=vendor&format=csv&period=30
Returns CSV file with order and revenue data for the requested period.
EP-11 · Admin Portal — Customer Management & Platform Analytics
Value
Platform administrators can now manage customers (view, suspend, reactivate), oversee all orders across the platform, view platform-wide analytics, and suspend/reactivate vendors.
Admin Portal UI (http://localhost:5001)
Dashboard (/) — now shows platform statistics summary (total vendors, customers, orders) with quick-access links to management sections.
Customers (/Customers/List) — searchable, paginated table of all registered customers. Click a customer to see their profile details. Use Suspend / Reactivate buttons to manage account status.
Orders (/Orders/List) — platform-wide order list with status filter tabs. Useful for monitoring order volume and identifying stuck orders.
Vendors (/Vendors/Details/{id}) — updated with Suspend and Reactivate buttons. Suspending a vendor disables their storefront and vase connections.
API endpoints (PlatformAdmin policy)
GET /api/v1/admin/customers?search=jane&page=1&pageSize=20
GET /api/v1/admin/customers/{id}
POST /api/v1/admin/customers/{id}/suspend
POST /api/v1/admin/customers/{id}/reactivate
GET /api/v1/admin/orders?status=Paid&page=1&pageSize=20
POST /api/v1/admin/vendors/{id}/suspend
POST /api/v1/admin/vendors/{id}/reactivate
EP-12 · Vendor Portal — Orders, Analytics & Profile
Value
Vendors can manage incoming orders, view revenue analytics, edit their business profile, configure opening hours, and upload bouquet photos — all from the portal UI. Critical bugs fixed: VendorId now reads from JWT instead of session; mock-vase upload replaced with real API call.
Vendor Portal UI (http://localhost:5002)
Orders (/Orders) — tabbed view (Pending / Confirmed / Ready / Completed). Click an order to see line items and timeline. Advance the status with one click: Confirm → Ready → Complete.
Analytics (/Analytics) — six stat cards showing revenue, order count, bouquets sold, average order value, active vases, and available bouquets.
Settings > Profile (/Settings/Profile) — edit business name, description, email, and phone.
Settings > Opening Hours (/Settings/OpeningHours) — weekly schedule editor. Select operating days and set open/close times. These hours feed the isCurrentlyOpen flag shown to customers.
Bugs fixed
| Bug | Fix |
|---|---|
| VendorId read from session | ICurrentVendorService reads vendor_id JWT claim (falls back to session) |
| Mock-vase raw HttpClient upload | Uses ApiClient.UploadBouquetPhotoAsync calling POST /api/v1/bouquets/{id}/photo |
EP-13 · Customer Web App — Authentication & Shopping
Value
Transforms the customer web app from a read-only browser into a full shopping experience with authentication, cart management, checkout, and an interactive map.
Customer App UI (http://localhost:5003)
Home page (/) — Leaflet.js interactive map centered on Warsaw (52.23, 21.01). Nearby bouquet markers load automatically. Click a marker to see the bouquet name, price, and a link to the detail page.
Cart (/Cart) — view items with prices, remove individual items, clear all, and proceed to checkout. Cart badge in the navigation shows item count.
Checkout (/Checkout) — order preview showing line items, delivery fee, and total. “Place Order” creates the order. Redirects to a confirmation page on success.
Confirmation (/Checkout/Confirmation/{orderId}) — displays order ID and a thank-you message.
Protected routes: Cart, Checkout, Orders, Favourites, Profile, and Subscriptions require authentication. Unauthenticated users are redirected to /Account/Login.
EP-14 · Customer Web App — Profile, Favorites & Subscriptions
Value
Gives logged-in customers full self-service: order history with timeline tracking, favourite bouquets, profile editing with saved addresses, and subscription management.
Customer App UI
Orders (/Orders) — list with status filter tabs (All / Pending / Confirmed / In Delivery / Completed / Cancelled). Click an order to see full detail including line items, status timeline, estimated delivery, and delivery QR code.
Favourites (/Favourites) — card grid of saved bouquets with availability badges (“Available”, “Sold”, “Expired”). Toggle favourites from any bouquet card.
Profile (/Profile) — view and edit name, phone, and notification preferences. Sidebar shows account stats (total orders, active subscriptions).
Addresses (/Profile/Addresses) — list saved delivery addresses with an inline add form. Delete addresses with one click.
Subscriptions (/Subscriptions) — list active subscriptions with type, criteria, and frequency. Create new subscriptions with a form. “Disable All” button turns off all notifications at once.
EP-15 · IoT Integration
Value
Moves IoT from simulation to production-readiness: real photo uploads, authenticated MQTT connections, smarter offline detection, and over-the-air firmware updates.
Bouquet photo upload
POST /api/v1/bouquets/{id}/photo
Authorization: Bearer <vendor-token>
Content-Type: multipart/form-data
# Form field: "photo" — jpg, png, or webp, max 5 MB
Returns { "photoUrl": "/uploads/bouquets/{bouquetId}_{filename}" }. The Vendor Portal’s vase details page now uses this endpoint instead of the mock-vase workaround.
MQTT broker authentication
The MQTT client now reads Mqtt:Username and Mqtt:Password from configuration and passes credentials when connecting to Mosquitto. Configure in appsettings.Development.json:
{
"Mqtt": {
"Host": "localhost",
"Port": 1883,
"Username": "flowershop",
"Password": "dev123"
}
}
Stale vase detection
ISmartVaseRepository.GetStaleVasesAsync(cutoffMinutes) queries vases whose last heartbeat exceeds the threshold and aren’t already marked offline. The VaseHealthMonitoringService uses this every 15 seconds to detect and report offline vases.
OTA firmware updates
POST /api/v1/smartvases/{id}/firmware
Authorization: Bearer <admin-token>
{
"firmwareUrl": "https://firmware.flowershop.dev/v2.1.0/vase.bin",
"version": "2.1.0"
}
Returns 202 Accepted. The server publishes an MQTT message to vase/{serialNumber}/ota containing the download URL. The vase firmware handles the update autonomously.
Quick Reference — Service URLs
| Service | URL | Credentials |
|---|---|---|
| API + Swagger | http://localhost:8080/swagger |
— |
| Admin Portal | http://localhost:5001 |
platformadmin / AdminPass123! |
| Vendor Portal | http://localhost:5002 |
(vendor accounts) |
| Customer App | http://localhost:5003 |
(customer accounts) |
| SignalR Hub | ws://localhost:8080/hubs/bouquet |
— |
| Stripe Webhook | POST http://localhost:8080/api/stripe/webhook |
Stripe-Signature header |
| Health Check | http://localhost:8080/health |
— |