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;
}
}
π 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; }
}
}
π 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;
}
}
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; }
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);
Step 3 β Run Migration
In terminal:
dotnet ef migrations add AddInvoiceLedgerEntities
dotnet ef database update
β
Check in psql:
\dt
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);
}
}
π 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;
}
}
}
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);
}
}
}
Step 6 β Register Service in Program.cs
Add:
builder.Services.AddScoped<IInvoiceService, InvoiceService>();
Step 7 β Run and Test
Run:
dotnet run
Open Swagger β https://localhost:5001/swagger
You should see:
GET /api/invoice
GET /api/invoice/{id}
POST /api/invoice
Try POST:
{
"customerId": 1,
"invoiceNumber": "INV001",
"items": [
{ "itemId": 1, "quantity": 2, "unitPrice": 500, "taxRate": 10, "subTotal": 1000 }
]
}
β
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)