DEV Community

Sugumar
Sugumar

Posted on

πŸ“… Day 5 β€” Invoice Module Base Setup β€”

Excellent, Sathish πŸ”₯πŸ‘

You’ve finished your User + Role (Day 4) perfectly.
Now we move to πŸ“… Day 5 β€” Invoice Module Base Setup β€”
this is where your project starts behaving like a real accounting system 🧾


🎯 Goal (Day 5)

Build the Invoice, InvoiceItem, and Ledger modules with relationships and basic CRUD APIs β€” fully tested in Swagger.


🧩 Entities Overview

We’ll work with 3 major entities:

Entity Description
Invoice Main invoice header (Customer, Date, Total)
InvoiceItem Line items of the invoice (Item, Quantity, Price, Tax, Subtotal)
Ledger Accounting journal entries (Debit, Credit, ReferenceType, ReferenceId)

βš™οΈ Step-by-Step Setup


Step 1 β€” Entity Classes

πŸ“ Accounting.Domain/Entities/Invoice.cs

namespace Accounting.Core.Entities
{
    public class Invoice
    {
        public int Id { get; set; }
        public int CustomerId { get; set; }
        public Customer? Customer { get; set; }

        public DateTime InvoiceDate { get; set; } = DateTime.UtcNow;
        public string InvoiceNumber { get; set; } = string.Empty;
        public decimal SubTotal { get; set; }
        public decimal TaxAmount { get; set; }
        public decimal TotalAmount { get; set; }
        public ICollection<InvoiceItem>? Items { get; set; }

        public DateTime CreatedAtUtc { get; set; } = DateTime.UtcNow;
    }
}
Enter fullscreen mode Exit fullscreen mode

πŸ“ Accounting.Domain/Entities/InvoiceItem.cs

namespace Accounting.Core.Entities
{
    public class InvoiceItem
    {
        public int Id { get; set; }
        public int InvoiceId { get; set; }
        public Invoice? Invoice { get; set; }

        public int ItemId { get; set; }
        public Item? Item { get; set; }

        public int Quantity { get; set; }
        public decimal UnitPrice { get; set; }
        public decimal TaxRate { get; set; }
        public decimal SubTotal { get; set; }
    }
}
Enter fullscreen mode Exit fullscreen mode

πŸ“ Accounting.Domain/Entities/Ledger.cs

namespace Accounting.Core.Entities
{
    public class Ledger
    {
        public int Id { get; set; }
        public string ReferenceType { get; set; } = string.Empty; // e.g., "Invoice" or "Expense"
        public int ReferenceId { get; set; }
        public string AccountName { get; set; } = string.Empty;
        public decimal Debit { get; set; }
        public decimal Credit { get; set; }
        public DateTime EntryDate { get; set; } = DateTime.UtcNow;
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 2 β€” Update DbContext

πŸ“ Accounting.Infrastructure/Data/AppDbContext.cs

Add:

public DbSet<Invoice> Invoices { get; set; }
public DbSet<InvoiceItem> InvoiceItems { get; set; }
public DbSet<Ledger> Ledgers { get; set; }
Enter fullscreen mode Exit fullscreen mode

and inside OnModelCreating():

// Invoice ↔ InvoiceItem (1:N)
modelBuilder.Entity<Invoice>()
    .HasMany(i => i.Items)
    .WithOne(ii => ii.Invoice)
    .HasForeignKey(ii => ii.InvoiceId)
    .OnDelete(DeleteBehavior.Cascade);
Enter fullscreen mode Exit fullscreen mode

Step 3 β€” Run Migration

In terminal:

dotnet ef migrations add AddInvoiceLedgerEntities
dotnet ef database update
Enter fullscreen mode Exit fullscreen mode

βœ… Check in psql:

\dt
Enter fullscreen mode Exit fullscreen mode

You’ll see Invoices, InvoiceItems, and Ledgers.


Step 4 β€” Service Layer

πŸ“ Accounting.API/Services/Interfaces/IInvoiceService.cs

using Accounting.Core.Entities;

namespace Accounting.API.Services.Interfaces
{
    public interface IInvoiceService
    {
        Task<IEnumerable<Invoice>> GetAllAsync();
        Task<Invoice?> GetByIdAsync(int id);
        Task<Invoice> CreateAsync(Invoice invoice);
    }
}
Enter fullscreen mode Exit fullscreen mode

πŸ“ Accounting.API/Services/Implementations/InvoiceService.cs

using Accounting.Core.Entities;
using Accounting.Infrastructure.Repositories.Interfaces;
using Accounting.API.Services.Interfaces;

namespace Accounting.API.Services.Implementations
{
    public class InvoiceService : IInvoiceService
    {
        private readonly IGenericRepository<Invoice> _invoiceRepo;
        private readonly IGenericRepository<Ledger> _ledgerRepo;
        private readonly IUnitOfWork _uow;

        public InvoiceService(
            IGenericRepository<Invoice> invoiceRepo,
            IGenericRepository<Ledger> ledgerRepo,
            IUnitOfWork uow)
        {
            _invoiceRepo = invoiceRepo;
            _ledgerRepo = ledgerRepo;
            _uow = uow;
        }

        public async Task<IEnumerable<Invoice>> GetAllAsync() => await _invoiceRepo.GetAllAsync();

        public async Task<Invoice?> GetByIdAsync(int id) => await _invoiceRepo.GetByIdAsync(id);

        public async Task<Invoice> CreateAsync(Invoice invoice)
        {
            // Calculate totals
            invoice.SubTotal = invoice.Items?.Sum(i => i.SubTotal) ?? 0;
            invoice.TaxAmount = invoice.Items?.Sum(i => i.SubTotal * i.TaxRate / 100) ?? 0;
            invoice.TotalAmount = invoice.SubTotal + invoice.TaxAmount;

            await _invoiceRepo.AddAsync(invoice);
            await _uow.SaveChangesAsync();

            // Ledger entry
            var ledger = new Ledger
            {
                ReferenceType = "Invoice",
                ReferenceId = invoice.Id,
                AccountName = "Sales",
                Debit = 0,
                Credit = invoice.TotalAmount
            };
            await _ledgerRepo.AddAsync(ledger);
            await _uow.SaveChangesAsync();

            return invoice;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 5 β€” Controller

πŸ“ Accounting.API/Controllers/InvoiceController.cs

using Accounting.API.Services.Interfaces;
using Accounting.Core.Entities;
using Microsoft.AspNetCore.Mvc;

namespace Accounting.API.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class InvoiceController : ControllerBase
    {
        private readonly IInvoiceService _invoiceService;
        public InvoiceController(IInvoiceService invoiceService)
        {
            _invoiceService = invoiceService;
        }

        [HttpGet]
        public async Task<IActionResult> GetAll() =>
            Ok(await _invoiceService.GetAllAsync());

        [HttpGet("{id}")]
        public async Task<IActionResult> GetById(int id)
        {
            var result = await _invoiceService.GetByIdAsync(id);
            return result == null ? NotFound() : Ok(result);
        }

        [HttpPost]
        public async Task<IActionResult> Create([FromBody] Invoice invoice)
        {
            var created = await _invoiceService.CreateAsync(invoice);
            return CreatedAtAction(nameof(GetById), new { id = created.Id }, created);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 6 β€” Register Service in Program.cs

Add:

builder.Services.AddScoped<IInvoiceService, InvoiceService>();
Enter fullscreen mode Exit fullscreen mode

Step 7 β€” Run and Test

Run:

dotnet run
Enter fullscreen mode Exit fullscreen mode

Open Swagger β†’ https://localhost:5001/swagger

You should see:

GET  /api/invoice
GET  /api/invoice/{id}
POST /api/invoice
Enter fullscreen mode Exit fullscreen mode

Try POST:

{
  "customerId": 1,
  "invoiceNumber": "INV001",
  "items": [
    { "itemId": 1, "quantity": 2, "unitPrice": 500, "taxRate": 10, "subTotal": 1000 }
  ]
}
Enter fullscreen mode Exit fullscreen mode

βœ… Database auto-calculates totals
βœ… Ledger gets a credit entry
βœ… InvoiceItems stored with invoice ID


βœ… End of Day 5 Deliverables

Feature Status
Invoice + Items + Ledger Entities βœ…
Repository + Service Layer βœ…
Invoice Controller βœ…
Ledger Auto Entry βœ…
Swagger Test βœ…

Next up β€” πŸ“… Day 6 β€” Authentication & Authorization (JWT + Login/Register + [Authorize])
Would you like me to prepare your Day 6 JWT setup plan next?

Top comments (0)