feat(expense): add per-expense FunctionalClass override
This commit is contained in:
@@ -248,6 +248,27 @@ public class ExpenseServiceTests
|
|||||||
Assert.Null(await db.Expenses.FirstOrDefaultAsync(e => e.Id == id));
|
Assert.Null(await db.Expenses.FirstOrDefaultAsync(e => e.Id == id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Create_PersistsFunctionalClass_AndGetReturnsIt()
|
||||||
|
{
|
||||||
|
var db = BuildDb("u1");
|
||||||
|
db.Ministries.Add(new ROLAC.API.Entities.Ministry { Id = 1, Name_en = "Admin" });
|
||||||
|
db.ExpenseCategoryGroups.Add(new ROLAC.API.Entities.ExpenseCategoryGroup { Id = 1, Name_en = "Other" });
|
||||||
|
db.ExpenseSubCategories.Add(new ROLAC.API.Entities.ExpenseSubCategory { Id = 1, GroupId = 1, Name_en = "Misc" });
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
var svc = SvcAs(db, new FakeStorage(), "u1");
|
||||||
|
|
||||||
|
var id = await svc.CreateAsync(new CreateExpenseRequest
|
||||||
|
{
|
||||||
|
Type = "VendorPayment", MinistryId = 1, CategoryGroupId = 1, SubCategoryId = 1,
|
||||||
|
Amount = 50m, Description = "x", ExpenseDate = new DateOnly(2026, 5, 1),
|
||||||
|
FunctionalClass = "ManagementGeneral",
|
||||||
|
}, isFinance: true);
|
||||||
|
|
||||||
|
var dto = await svc.GetByIdAsync(id);
|
||||||
|
Assert.Equal("ManagementGeneral", dto!.FunctionalClass);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Receipt_SaveThenOpen_RoundTrips()
|
public async Task Receipt_SaveThenOpen_RoundTrips()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ public class ExpenseListItemDto
|
|||||||
public string ExpenseDate { get; set; } = ""; // yyyy-MM-dd
|
public string ExpenseDate { get; set; } = ""; // yyyy-MM-dd
|
||||||
public bool HasReceipt { get; set; }
|
public bool HasReceipt { get; set; }
|
||||||
public string? CheckNumber { get; set; }
|
public string? CheckNumber { get; set; }
|
||||||
|
public string? FunctionalClass { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ExpenseDto : ExpenseListItemDto
|
public class ExpenseDto : ExpenseListItemDto
|
||||||
@@ -45,6 +46,7 @@ public class CreateExpenseRequest
|
|||||||
[MaxLength(50)] public string? CheckNumber { get; set; }
|
[MaxLength(50)] public string? CheckNumber { get; set; }
|
||||||
[Required] public DateOnly ExpenseDate { get; set; }
|
[Required] public DateOnly ExpenseDate { get; set; }
|
||||||
public string? Notes { get; set; }
|
public string? Notes { get; set; }
|
||||||
|
[MaxLength(20)] public string? FunctionalClass { get; set; }
|
||||||
}
|
}
|
||||||
public class UpdateExpenseRequest : CreateExpenseRequest { }
|
public class UpdateExpenseRequest : CreateExpenseRequest { }
|
||||||
|
|
||||||
|
|||||||
@@ -246,6 +246,7 @@ public class AppDbContext : IdentityDbContext<AppUser, AppRole, string>
|
|||||||
|
|
||||||
entity.Property(e => e.Type).HasMaxLength(30).IsRequired();
|
entity.Property(e => e.Type).HasMaxLength(30).IsRequired();
|
||||||
entity.Property(e => e.Status).HasMaxLength(30).HasDefaultValue("Draft");
|
entity.Property(e => e.Status).HasMaxLength(30).HasDefaultValue("Draft");
|
||||||
|
entity.Property(e => e.FunctionalClass).HasMaxLength(20);
|
||||||
entity.Property(e => e.Amount).HasColumnType("decimal(18,2)");
|
entity.Property(e => e.Amount).HasColumnType("decimal(18,2)");
|
||||||
entity.Property(e => e.Description).HasMaxLength(500).IsRequired();
|
entity.Property(e => e.Description).HasMaxLength(500).IsRequired();
|
||||||
entity.Property(e => e.VendorName).HasMaxLength(200);
|
entity.Property(e => e.VendorName).HasMaxLength(200);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ public class Expense : SoftDeleteEntity, IAuditable
|
|||||||
public int SubCategoryId { get; set; }
|
public int SubCategoryId { get; set; }
|
||||||
public string Type { get; set; } = "StaffReimbursement"; // VendorPayment | StaffReimbursement
|
public string Type { get; set; } = "StaffReimbursement"; // VendorPayment | StaffReimbursement
|
||||||
public string Status { get; set; } = "Draft"; // see state machine
|
public string Status { get; set; } = "Draft"; // see state machine
|
||||||
|
public string? FunctionalClass { get; set; } // null = inherit Ministry.DefaultFunctionalClass
|
||||||
public decimal Amount { get; set; }
|
public decimal Amount { get; set; }
|
||||||
public string Description { get; set; } = null!;
|
public string Description { get; set; } = null!;
|
||||||
public string? VendorName { get; set; }
|
public string? VendorName { get; set; }
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ public class ExpenseService : IExpenseService
|
|||||||
ExpenseDate = e.ExpenseDate.ToString("yyyy-MM-dd"),
|
ExpenseDate = e.ExpenseDate.ToString("yyyy-MM-dd"),
|
||||||
HasReceipt = e.ReceiptBlobPath != null,
|
HasReceipt = e.ReceiptBlobPath != null,
|
||||||
CheckNumber = e.CheckNumber,
|
CheckNumber = e.CheckNumber,
|
||||||
|
FunctionalClass = e.FunctionalClass,
|
||||||
}).ToList();
|
}).ToList();
|
||||||
|
|
||||||
return new PagedResult<ExpenseListItemDto> { Items = items, TotalCount = total, Page = page, PageSize = pageSize };
|
return new PagedResult<ExpenseListItemDto> { Items = items, TotalCount = total, Page = page, PageSize = pageSize };
|
||||||
@@ -122,6 +123,7 @@ public class ExpenseService : IExpenseService
|
|||||||
ExpenseDate = e.ExpenseDate.ToString("yyyy-MM-dd"), HasReceipt = e.ReceiptBlobPath != null,
|
ExpenseDate = e.ExpenseDate.ToString("yyyy-MM-dd"), HasReceipt = e.ReceiptBlobPath != null,
|
||||||
CheckNumber = e.CheckNumber, Notes = e.Notes, ReviewNotes = e.ReviewNotes,
|
CheckNumber = e.CheckNumber, Notes = e.Notes, ReviewNotes = e.ReviewNotes,
|
||||||
SubmittedBy = e.SubmittedBy, SubmittedAt = e.SubmittedAt, ReviewedAt = e.ReviewedAt, PaidAt = e.PaidAt,
|
SubmittedBy = e.SubmittedBy, SubmittedAt = e.SubmittedAt, ReviewedAt = e.ReviewedAt, PaidAt = e.PaidAt,
|
||||||
|
FunctionalClass = e.FunctionalClass,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,6 +134,7 @@ public class ExpenseService : IExpenseService
|
|||||||
MinistryId = r.MinistryId, CategoryGroupId = r.CategoryGroupId, SubCategoryId = r.SubCategoryId,
|
MinistryId = r.MinistryId, CategoryGroupId = r.CategoryGroupId, SubCategoryId = r.SubCategoryId,
|
||||||
Type = r.Type, Amount = r.Amount, Description = r.Description, VendorName = r.VendorName,
|
Type = r.Type, Amount = r.Amount, Description = r.Description, VendorName = r.VendorName,
|
||||||
CheckNumber = r.CheckNumber, ExpenseDate = r.ExpenseDate, Notes = r.Notes,
|
CheckNumber = r.CheckNumber, ExpenseDate = r.ExpenseDate, Notes = r.Notes,
|
||||||
|
FunctionalClass = r.FunctionalClass,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (r.Type == "VendorPayment")
|
if (r.Type == "VendorPayment")
|
||||||
@@ -179,7 +182,7 @@ public class ExpenseService : IExpenseService
|
|||||||
|
|
||||||
e.MinistryId = r.MinistryId; e.CategoryGroupId = r.CategoryGroupId; e.SubCategoryId = r.SubCategoryId;
|
e.MinistryId = r.MinistryId; e.CategoryGroupId = r.CategoryGroupId; e.SubCategoryId = r.SubCategoryId;
|
||||||
e.Amount = r.Amount; e.Description = r.Description; e.CheckNumber = r.CheckNumber;
|
e.Amount = r.Amount; e.Description = r.Description; e.CheckNumber = r.CheckNumber;
|
||||||
e.ExpenseDate = r.ExpenseDate; e.Notes = r.Notes;
|
e.ExpenseDate = r.ExpenseDate; e.Notes = r.Notes; e.FunctionalClass = r.FunctionalClass;
|
||||||
if (e.Type == "VendorPayment") e.VendorName = r.VendorName;
|
if (e.Type == "VendorPayment") e.VendorName = r.VendorName;
|
||||||
await _db.SaveChangesAsync();
|
await _db.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user