Backend API for Morii Coffee, built on .NET 10 with Clean Architecture, CQRS, EF Core, PostgreSQL, Redis, Stripe, Hangfire, and a GHN shipping integration.
source/MoriiCoffee.Presentationhosts the ASP.NET Core API, middleware, Swagger, health checks, and Hangfire dashboard.source/MoriiCoffee.Applicationcontains MediatR commands/queries, validators, DTOs, and application services.source/MoriiCoffee.Domainandsource/MoriiCoffee.Domain.Sharedhold aggregates, enums, and business rules.source/MoriiCoffee.Infrastructureandsource/MoriiCoffee.Infrastructure.Persistenceimplement external integrations, repositories, caching, background jobs, and migrations.
- Authentication with JWT and Google OAuth.
- Catalog, category, banner, store, wishlist, and blog management.
- Redis-backed cart for authenticated users.
- Order lifecycle management for pickup and GHN delivery.
- Stripe payment-first checkout, reconciliation, refunds, and audited webhooks.
- GHN shipping quote, shipment creation, requote, sync, cancel, and webhook ingestion.
- File upload/download through MinIO and AWS S3-backed services.
- Admin reports and scheduled order auto-completion through Hangfire.
Presentation
-> controllers, HTTP pipeline, middleware, Swagger, Hangfire dashboard
Infrastructure / Infrastructure.Persistence
-> payment, shipping, storage, email, Redis, EF Core, repositories, migrations
Application
-> commands, queries, validators, DTOs, orchestration services
Domain / Domain.Shared
-> aggregates, entities, enums, settings contracts, invariants
The codebase follows a standard inward dependency direction: outer layers depend on inner layers, not the reverse.
- .NET 10 SDK
- Docker Desktop
cd deploy
bash run-docker-development.shThis starts:
- API at
http://localhost:8002 - Swagger at
http://localhost:8002/swagger - Hangfire dashboard at
http://localhost:8002/hangfire - PostgreSQL at
localhost:5432 - Redis at
localhost:6379 - MinIO console at
http://localhost:9001
The local compose files are:
deploy/docker-compose.yml: PostgreSQL, Redis, MinIOdeploy/docker-compose.development.yml: API container overlaydeploy/run-docker-development.sh: convenience wrapper
The runtime configuration pipeline is defined in HostExtensions.cs:
- base
appsettings.json - environment override
appsettings.{Environment}.json - environment variables
- user secrets in
Development
Operationally, prefer environment variables and user secrets for sensitive values. README intentionally does not duplicate the appsettings.json shape.
At startup the API:
- registers infrastructure services and Serilog in
Program.cs - enables Swagger, forwarded headers, CORS, auth, controllers,
/health, and Hangfire dashboard inApplicationExtensions.cs - auto-applies pending EF Core migrations and runs
ApplicationDbContextSeed.SeedAsync()on boot inApplicationExtensions.cs - registers the recurring
order-auto-completeHangfire job inHangfireJobsExtensions.cs
The repository contains one production pipeline in deploy.yml:
Deployment diagram:
- push to
main - run
dotnet testforMoriiCoffee.Application.TestsandMoriiCoffee.Domain.Tests - build the
finaltarget fromsource/MoriiCoffee.Presentation/Dockerfile - push the image to AWS ECR
- SSH into EC2
- run
bash /app/fetch-ssm-env.sh - run
bash /app/run-container.sh <image> - prune old images
Operational implications of the current production path:
- The repo documents a remote fetch from AWS SSM, but the scripts invoked on EC2 are not stored in this repository.
- The application itself also supports checked-in environment JSON files plus environment variables; the deploy pipeline does not remove that behavior.
- Database migrations and seeding happen inside application startup, not in a separate release step.
- The Hangfire dashboard is mounted by default; this repo does not add a dashboard authorization filter.
/healthis available as a simple runtime health endpoint.
Cart, order checkout, payment, and shipping flow:
The cart is implemented by RedisCartService.cs.
- Storage key:
cart:{userId} - Value: serialized
CartDto - TTL:
CacheTtlConstants.Cart - Duplicate product/variant pairs are merged by incrementing quantity.
- Guest cart merge is handled through
POST /api/v1/cart/merge.
Controller surface:
GET /api/v1/cartPOST /api/v1/cart/itemsPUT /api/v1/cart/itemsDELETE /api/v1/cart/itemsDELETE /api/v1/cartPOST /api/v1/cart/merge
Order entry points live in OrdersController.cs.
POST /api/v1/ordersis for immediate order creation flows such as COD.- Stripe orders are not created here; they are finalized only after payment confirmation.
- GHN delivery orders require a shipping quote fingerprint, service selection, fee, expiry, and provider environment.
- Admins can fetch all orders, update status, and inspect valid next statuses.
PlaceOrderCommandHandler in PlaceOrderCommandHandler.cs does the main orchestration:
- load cart from Redis
- create an order snapshot from cart items
- validate the GHN quote fingerprint for delivery orders
- persist the order and optionally upsert the saved delivery profile
- attempt shipment creation for GHN orders
- clear the cart
Payment HTTP endpoints live in PaymentsController.cs and PaymentWebhookController.cs.
Implemented flow:
POST /api/v1/payments/stripe/checkout-sessionsnapshots the current cart and checkout data into a cached draft.- Stripe-hosted checkout completes payment.
POST /api/v1/payments/stripe/webhookverifies the Stripe signature and finalizes or reconciles state.POST /api/v1/payments/stripe/reconcilelets the frontend self-heal when the success redirect arrives before webhook processing.
Relevant implementation points:
- Draft caching and order finalization:
StripeCheckoutDraftService.cs - Stripe gateway implementation:
StripePaymentGateway.cs - Webhook auditing and idempotency:
HandleWebhookEventCommandHandler.cs
Shipping endpoints live in ShippingController.cs and ShippingWebhookController.cs.
Implemented GHN flow:
- fetch provinces, districts, and wards
- quote shipping from the current cart
- place a COD order or finalize a Stripe-paid order
- create a GHN shipment using the stored order snapshot
- sync, requote, cancel, or update shipment note through admin endpoints
- accept GHN webhook updates and persist shipment audit rows
Core orchestration lives in:
- quote creation:
CreateShippingQuoteCommandHandler.cs - shipment lifecycle:
ShipmentLifecycleService.cs - webhook processing:
HandleShippingWebhookEventCommandHandler.cs
Hangfire is configured in HangfireConfiguration.cs with PostgreSQL storage.
The recurring job currently registered is:
order-auto-complete: runs daily at0 {AutoCompleteJobRunHour} * * *and executesOrderAutoCompleteJob.cs
Main route groups:
/api/v1/auth/api/v1/products/api/v1/categories/api/v1/banners/api/v1/stores/api/v1/cart/api/v1/orders/api/v1/payments/api/v1/shipping/api/v1/users/api/v1/files/api/v1/admin-reports
Swagger is available in development at /swagger.
Run the main backend test suites with:
dotnet test source/MoriiCoffee.Domain.Tests --configuration Release
dotnet test source/MoriiCoffee.Application.Tests --configuration ReleaseCoverage helper:
bash coverage.shmorii-coffee/
├── .github/workflows/
├── deploy/
├── specs/
├── source/
│ ├── MoriiCoffee.Application/
│ ├── MoriiCoffee.Application.Tests/
│ ├── MoriiCoffee.DbMigrator/
│ ├── MoriiCoffee.Domain/
│ ├── MoriiCoffee.Domain.Shared/
│ ├── MoriiCoffee.Domain.Tests/
│ ├── MoriiCoffee.Infrastructure/
│ ├── MoriiCoffee.Infrastructure.Persistence/
│ └── MoriiCoffee.Presentation/
├── coverage.sh
├── Directory.Build.props
├── global.json
└── MoriiCoffee.slnx

