Dec 2025 – Present
MangaKaCR POS & Inventory Platform
The point-of-sale, inventory and back-office platform that runs MangaKaCR, a manga bookstore in San José. I designed and built every part of it on my own.
My roleSole developer: architecture, backend, frontend, testing, CI/CD and production operations
- automated tests
- 916
- automated tests
- app screens
- 28
- app screens
- business modules
- 10
- business modules

MangaKaCR is an independent manga bookstore in San José. It used to run its counter sales and invoicing on a legacy system; I replaced it with a platform built from scratch around how the store actually works: counter sales with several payment methods, layaways, cash-register shifts, bulk restocks, client cashback, back-in-stock alerts and a printable catalog that updates itself every day.
I was the only person on the project. I designed the architecture and domain model, wrote the API, the admin web app, the PDFs and emails, the 916 automated tests and the CI/CD pipelines, migrated the infrastructure mid-project, and I deploy and support it in production. The store let me publish it here.
Architecture
The system is two .NET 9 applications, each split into clean-architecture layers (Domain → Application → Infrastructure → Presentation, plus a composition project for dependency injection):
- Mangaka.Api: an ASP.NET Core Minimal API with 20 endpoint groups, JWT authentication, an administrator-only policy, global ProblemDetails error handling and two background services.
- Mangaka.Invoicing: the staff-facing Blazor Server app (MudBlazor + ApexCharts) with 28 screens and ~100 Razor components. It talks to the API through a typed client generated with Microsoft Kiota from the API's OpenAPI document, so a contract change breaks the build instead of production.
- A rich domain model: entities and self-validating value objects (Price, Cost, CABYS code, cédula, email, phone, storage location…) with domain exceptions translated to HTTP responses at the edge.
- PostgreSQL with 34 EF Core migrations, product images in Cloudflare R2, transactional email through Resend, documents with QuestPDF, a daily Google Drive sync and lookups against Costa Rica's Hacienda API.
Point of sale
- A multi-tab POS: each vendor's open draft invoices come back as tabs, scoped to whoever is signed in (I fixed a bug where carts could leak between vendors).
- Sales to a walk-in client or a registered one. Typing a cédula fetches the registered name from Hacienda's public API, cached for an hour (including not-found results) to avoid hammering the service.
- Six payment methods (cash with change, card, bank transfer, SINPE Móvil, CompraClick) plus combined payments split across methods, per-line price overrides and time-limited offers by amount or percentage.
- Emitting an invoice decrements stock and assigns its consecutive number, continuing the legacy system's numbering through a dedicated migration so the store's sequence never broke.
- Voids within a 24-hour window, administrators only: stock is restored, quantity-limited offers are credited back and an audit entry is recorded.
- 80 mm POS tickets and A4 receipts generated with QuestPDF, receipts emailed to the client, and a combined A4 PDF export of many receipts at once.
- Client cashback: 10% accrues on each sale, the whole balance can be redeemed (capped at the total), it's reversed on voids, and the cashier can opt a sale out. Layaway payoffs deliberately don't earn it.
Getting concurrency right
The trickiest bug I tracked down: to keep the cart responsive, the POS sends one request per product in parallel when it flushes a burst of edits. Each request loaded the invoice, changed its own line and wrote back totals computed from whatever lines it had seen. That is a classic lost update: a live 9-line cart emitted with the total of only 2 lines, even though all 9 lines were saved.
I fixed it with a PostgreSQL transaction-scoped advisory lock keyed on the invoice number, taken before the invoice is even read. Edits to the same invoice are serialized, different invoices still run in parallel, and the lock releases itself on commit or rollback.
- Other races I fixed: product quantity and storage-location updates, a search box that dropped keystrokes while a page was loading, and infinite scroll returning duplicate or missing rows under concurrent edits.
- Live sync across open tabs: an in-process pub/sub over Blazor Server's circuits pushes stock, price, location and offer changes to every open tab without reloading. An edit page shows a notice instead of overwriting what you're typing.
Layaways and cash-register shifts
- Apartados (layaways) reserve stock and lock each line's price at creation, collect an initial payment and later partial payments, enforce an expiration at least 30 days out, return stock if cancelled and finally convert into a full invoice, with their own tickets and receipts.
- Turnos (shifts) open with a starting cash amount, log manual cash-in movements and close with the counted cash and observations. Anything that collects money is blocked while no shift is open.
- Shift reports reconcile sales and layaway payments per period, covered by integration tests against a real database.
Inventory and catalog
- Products with validated CABYS tax codes, authors, categories, collections (with alternate names), publishers, demographics, types, storage location, visibility and image uploads to Cloudflare R2 with a crop-and-manage UI.
- Search ranked by match quality, filters, sorting and infinite scroll, plus bulk delete, price and visibility actions and one-click product duplication.
- Bulk restock drafts: a restock can be saved and resumed. Applying it updates every product's stock and location, writes one audit entry per change and freezes its report.
- “Avisar cuando llegue”: clients can watch sold-out products. Only a genuine out-of-stock to in-stock transition queues them, and a background service bundles everything that came back for a client into a single email after a one-hour window.
- A background service renders the full Product List PDF every morning and overwrites a fixed Google Drive file, so the link the store shares with customers never changes.
Analytics, administration and auditing
- Sales analytics with KPIs, top-10 breakdowns by author, collection, category, publisher, demographic, product, client and vendor, daily/weekly/monthly trends and previous-period or year-over-year comparisons, all in Costa Rica time.
- Sign-in through Neon Auth. The first sign-in creates an access request that an administrator approves (assigning a role) or rejects. Administrators can't change their own role or status.
- Searchable audit trails for products (with field-level change detail), clients, sales and invoices, plus company settings (tax regime, economic activity, terms and notes) that print on every receipt.
Quality, delivery and operations
- 916 automated tests across five projects: domain, application (Moq), infrastructure, presentation, and 87 integration tests that run against a real PostgreSQL in Testcontainers for the risky paths (cashback, layaway payoffs, shift reconciliation, cascade deletes).
- GitHub Actions for build and test, CodeQL analysis, and Docker images for both apps pushed to GHCR on every merge to main, built on a self-hosted Docker runner I set up. Local SonarQube analysis with coverage.
- Mid-project I migrated images from Supabase Storage to Cloudflare R2 and auth and database to Neon, bridging Neon's EdDSA-signed JWTs into .NET's token validation.
- Because the store's .cr registrar can't delegate DNS to Cloudflare, I serve the R2 images through a Cloudflare Worker with edge caching instead of a custom domain.
- A Git flow with feature branches, versioned releases promoted from development to main, and a published import schema for bulk-loading products crawled from the store's online shop. Next up: connecting the e-commerce storefront through a backend-for-frontend.
Built with
Next project
LASCE Research Lab Platform →



