Completely optimmized the Action API
This commit is contained in:
parent
282d33d8b2
commit
f9213b6040
@ -11,18 +11,18 @@ namespace Marco.Pms.CacheHelper
|
|||||||
private readonly IMongoDatabase _mongoDatabase;
|
private readonly IMongoDatabase _mongoDatabase;
|
||||||
public UpdateLogHelper(IConfiguration configuration)
|
public UpdateLogHelper(IConfiguration configuration)
|
||||||
{
|
{
|
||||||
var connectionString = configuration["MongoDB:ConnectionString"];
|
var connectionString = configuration["MongoDB:ModificationConnectionString"];
|
||||||
var mongoUrl = new MongoUrl(connectionString);
|
var mongoUrl = new MongoUrl(connectionString);
|
||||||
var client = new MongoClient(mongoUrl); // Your MongoDB connection string
|
var client = new MongoClient(mongoUrl); // Your MongoDB connection string
|
||||||
_mongoDatabase = client.GetDatabase(mongoUrl.DatabaseName); // Your MongoDB Database name
|
_mongoDatabase = client.GetDatabase(mongoUrl.DatabaseName); // Your MongoDB Database name
|
||||||
}
|
}
|
||||||
public async Task PushToUpdateLogs(UpdateLogsObject oldObject, string collectionName)
|
public async Task PushToUpdateLogsAsync(UpdateLogsObject oldObject, string collectionName)
|
||||||
{
|
{
|
||||||
var collection = _mongoDatabase.GetCollection<UpdateLogsObject>(collectionName);
|
var collection = _mongoDatabase.GetCollection<UpdateLogsObject>(collectionName);
|
||||||
await collection.InsertOneAsync(oldObject);
|
await collection.InsertOneAsync(oldObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<UpdateLogsObject>> GetFromUpdateLogsByEntityId(Guid entityId, string collectionName)
|
public async Task<List<UpdateLogsObject>> GetFromUpdateLogsByEntityIdAsync(Guid entityId, string collectionName)
|
||||||
{
|
{
|
||||||
var collection = _mongoDatabase.GetCollection<UpdateLogsObject>(collectionName);
|
var collection = _mongoDatabase.GetCollection<UpdateLogsObject>(collectionName);
|
||||||
var filter = Builders<UpdateLogsObject>.Filter.Eq(p => p.EntityId, entityId.ToString());
|
var filter = Builders<UpdateLogsObject>.Filter.Eq(p => p.EntityId, entityId.ToString());
|
||||||
@ -34,7 +34,7 @@ namespace Marco.Pms.CacheHelper
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<UpdateLogsObject>> GetFromUpdateLogsByUpdetedById(Guid updatedById, string collectionName)
|
public async Task<List<UpdateLogsObject>> GetFromUpdateLogsByUpdetedByIdAsync(Guid updatedById, string collectionName)
|
||||||
{
|
{
|
||||||
var collection = _mongoDatabase.GetCollection<UpdateLogsObject>(collectionName);
|
var collection = _mongoDatabase.GetCollection<UpdateLogsObject>(collectionName);
|
||||||
var filter = Builders<UpdateLogsObject>.Filter.Eq(p => p.UpdatedById, updatedById.ToString());
|
var filter = Builders<UpdateLogsObject>.Filter.Eq(p => p.UpdatedById, updatedById.ToString());
|
||||||
@ -46,7 +46,7 @@ namespace Marco.Pms.CacheHelper
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public BsonDocument NormalizeGuidsToStrings(object entity)
|
public BsonDocument EntityToBsonDocument(object entity)
|
||||||
{
|
{
|
||||||
var bson = new BsonDocument();
|
var bson = new BsonDocument();
|
||||||
|
|
||||||
@ -73,7 +73,7 @@ namespace Marco.Pms.CacheHelper
|
|||||||
var array = new BsonArray();
|
var array = new BsonArray();
|
||||||
foreach (var item in list)
|
foreach (var item in list)
|
||||||
{
|
{
|
||||||
array.Add(NormalizeGuidsToStrings(item)); // recursive
|
array.Add(EntityToBsonDocument(item)); // recursive
|
||||||
}
|
}
|
||||||
bson[prop.Name] = array;
|
bson[prop.Name] = array;
|
||||||
}
|
}
|
||||||
|
@ -393,7 +393,9 @@ namespace Marco.Pms.DataAccess.Data
|
|||||||
{
|
{
|
||||||
Id = Guid.Parse("297e0d8f-f668-41b5-bfea-e03b354251c8"),
|
Id = Guid.Parse("297e0d8f-f668-41b5-bfea-e03b354251c8"),
|
||||||
Name = "Draft",
|
Name = "Draft",
|
||||||
|
DisplayName = "Draft",
|
||||||
Description = "Expense has been created but not yet submitted.",
|
Description = "Expense has been created but not yet submitted.",
|
||||||
|
Color = "#212529",
|
||||||
IsSystem = true,
|
IsSystem = true,
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
TenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26")
|
TenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26")
|
||||||
@ -402,7 +404,9 @@ namespace Marco.Pms.DataAccess.Data
|
|||||||
{
|
{
|
||||||
Id = Guid.Parse("6537018f-f4e9-4cb3-a210-6c3b2da999d7"),
|
Id = Guid.Parse("6537018f-f4e9-4cb3-a210-6c3b2da999d7"),
|
||||||
Name = "Review Pending",
|
Name = "Review Pending",
|
||||||
|
DisplayName = "Review",
|
||||||
Description = "Reviewer is currently reviewing the expense.",
|
Description = "Reviewer is currently reviewing the expense.",
|
||||||
|
Color = "#0d6efd",
|
||||||
IsSystem = true,
|
IsSystem = true,
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
TenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26")
|
TenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26")
|
||||||
@ -411,7 +415,9 @@ namespace Marco.Pms.DataAccess.Data
|
|||||||
{
|
{
|
||||||
Id = Guid.Parse("4068007f-c92f-4f37-a907-bc15fe57d4d8"),
|
Id = Guid.Parse("4068007f-c92f-4f37-a907-bc15fe57d4d8"),
|
||||||
Name = "Approval Pending",
|
Name = "Approval Pending",
|
||||||
|
DisplayName = "Approve",
|
||||||
Description = "Review is completed, waiting for action of approver.",
|
Description = "Review is completed, waiting for action of approver.",
|
||||||
|
Color = "#0dcaf0",
|
||||||
IsSystem = true,
|
IsSystem = true,
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
TenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26")
|
TenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26")
|
||||||
@ -420,7 +426,9 @@ namespace Marco.Pms.DataAccess.Data
|
|||||||
{
|
{
|
||||||
Id = Guid.Parse("d1ee5eec-24b6-4364-8673-a8f859c60729"),
|
Id = Guid.Parse("d1ee5eec-24b6-4364-8673-a8f859c60729"),
|
||||||
Name = "Rejected",
|
Name = "Rejected",
|
||||||
|
DisplayName = "Reject",
|
||||||
Description = "Expense was declined, often with a reason(either review rejected or approval rejected.",
|
Description = "Expense was declined, often with a reason(either review rejected or approval rejected.",
|
||||||
|
Color = "#dc3545",
|
||||||
IsSystem = true,
|
IsSystem = true,
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
TenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26")
|
TenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26")
|
||||||
@ -429,7 +437,9 @@ namespace Marco.Pms.DataAccess.Data
|
|||||||
{
|
{
|
||||||
Id = Guid.Parse("f18c5cfd-7815-4341-8da2-2c2d65778e27"),
|
Id = Guid.Parse("f18c5cfd-7815-4341-8da2-2c2d65778e27"),
|
||||||
Name = "Process Pending",
|
Name = "Process Pending",
|
||||||
|
DisplayName = "Process",
|
||||||
Description = "Approved expense is awaiting final payment.",
|
Description = "Approved expense is awaiting final payment.",
|
||||||
|
Color = "#ffc107",
|
||||||
IsSystem = true,
|
IsSystem = true,
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
TenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26")
|
TenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26")
|
||||||
@ -438,7 +448,9 @@ namespace Marco.Pms.DataAccess.Data
|
|||||||
{
|
{
|
||||||
Id = Guid.Parse("61578360-3a49-4c34-8604-7b35a3787b95"),
|
Id = Guid.Parse("61578360-3a49-4c34-8604-7b35a3787b95"),
|
||||||
Name = "Processed",
|
Name = "Processed",
|
||||||
|
DisplayName = "Paid",
|
||||||
Description = "Expense has been settled.",
|
Description = "Expense has been settled.",
|
||||||
|
Color = "#198754",
|
||||||
IsSystem = true,
|
IsSystem = true,
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
TenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26")
|
TenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26")
|
||||||
@ -451,7 +463,7 @@ namespace Marco.Pms.DataAccess.Data
|
|||||||
{
|
{
|
||||||
Id = Guid.Parse("5cf7f1df-9d1f-4289-add0-1775ad614f25"),
|
Id = Guid.Parse("5cf7f1df-9d1f-4289-add0-1775ad614f25"),
|
||||||
StatusId = Guid.Parse("f18c5cfd-7815-4341-8da2-2c2d65778e27"),
|
StatusId = Guid.Parse("f18c5cfd-7815-4341-8da2-2c2d65778e27"),
|
||||||
NextStatusId = Guid.Parse("f18c5cfd-7815-4341-8da2-2c2d65778e27"),
|
NextStatusId = Guid.Parse("61578360-3a49-4c34-8604-7b35a3787b95"),
|
||||||
TenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26")
|
TenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26")
|
||||||
},
|
},
|
||||||
// Approve to Rejected
|
// Approve to Rejected
|
||||||
@ -470,6 +482,14 @@ namespace Marco.Pms.DataAccess.Data
|
|||||||
NextStatusId = Guid.Parse("f18c5cfd-7815-4341-8da2-2c2d65778e27"),
|
NextStatusId = Guid.Parse("f18c5cfd-7815-4341-8da2-2c2d65778e27"),
|
||||||
TenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26")
|
TenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26")
|
||||||
},
|
},
|
||||||
|
// Rejected to Review
|
||||||
|
new ExpensesStatusMapping
|
||||||
|
{
|
||||||
|
Id = Guid.Parse("75bbda6a-6a53-47d1-ad71-5f5f9446a11e"),
|
||||||
|
StatusId = Guid.Parse("d1ee5eec-24b6-4364-8673-a8f859c60729"),
|
||||||
|
NextStatusId = Guid.Parse("6537018f-f4e9-4cb3-a210-6c3b2da999d7"),
|
||||||
|
TenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26")
|
||||||
|
},
|
||||||
// Review to Rejected
|
// Review to Rejected
|
||||||
new ExpensesStatusMapping
|
new ExpensesStatusMapping
|
||||||
{
|
{
|
||||||
@ -496,6 +516,48 @@ namespace Marco.Pms.DataAccess.Data
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
modelBuilder.Entity<StatusPermissionMapping>().HasData(
|
||||||
|
|
||||||
|
// Approval Pending Permission Mapping
|
||||||
|
new StatusPermissionMapping
|
||||||
|
{
|
||||||
|
Id = Guid.Parse("ed893799-1a5f-4311-a077-de93c86ca8fd"),
|
||||||
|
PermissionId = Guid.Parse("1f4bda08-1873-449a-bb66-3e8222bd871b"),
|
||||||
|
StatusId = Guid.Parse("4068007f-c92f-4f37-a907-bc15fe57d4d8"),
|
||||||
|
TenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26")
|
||||||
|
},
|
||||||
|
// Rejected Permission Mapping
|
||||||
|
new StatusPermissionMapping
|
||||||
|
{
|
||||||
|
Id = Guid.Parse("4652d73f-fc71-4fe1-9f2f-1e48b342d741"),
|
||||||
|
PermissionId = Guid.Parse("1f4bda08-1873-449a-bb66-3e8222bd871b"),
|
||||||
|
StatusId = Guid.Parse("d1ee5eec-24b6-4364-8673-a8f859c60729"),
|
||||||
|
TenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26")
|
||||||
|
},
|
||||||
|
new StatusPermissionMapping
|
||||||
|
{
|
||||||
|
Id = Guid.Parse("cd15f9b9-be45-4deb-9c71-2f23f872dbcd"),
|
||||||
|
PermissionId = Guid.Parse("eaafdd76-8aac-45f9-a530-315589c6deca"),
|
||||||
|
StatusId = Guid.Parse("d1ee5eec-24b6-4364-8673-a8f859c60729"),
|
||||||
|
TenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26")
|
||||||
|
},
|
||||||
|
// Process Pending Permission Mapping
|
||||||
|
new StatusPermissionMapping
|
||||||
|
{
|
||||||
|
Id = Guid.Parse("f6f26b2f-2fa6-40b7-8601-cbd4bcdda0cc"),
|
||||||
|
PermissionId = Guid.Parse("eaafdd76-8aac-45f9-a530-315589c6deca"),
|
||||||
|
StatusId = Guid.Parse("f18c5cfd-7815-4341-8da2-2c2d65778e27"),
|
||||||
|
TenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26")
|
||||||
|
},
|
||||||
|
// Processed Permission Mapping
|
||||||
|
new StatusPermissionMapping
|
||||||
|
{
|
||||||
|
Id = Guid.Parse("214354e5-daad-4569-ad69-eb5bf4e87fbc"),
|
||||||
|
PermissionId = Guid.Parse("ea5a1529-4ee8-4828-80ea-0e23c9d4dd11"),
|
||||||
|
StatusId = Guid.Parse("61578360-3a49-4c34-8604-7b35a3787b95"),
|
||||||
|
TenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26")
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity<ExpensesTypeMaster>().HasData(
|
modelBuilder.Entity<ExpensesTypeMaster>().HasData(
|
||||||
new ExpensesTypeMaster
|
new ExpensesTypeMaster
|
||||||
{
|
{
|
||||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,63 +0,0 @@
|
|||||||
using System;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Marco.Pms.DataAccess.Migrations
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class Added_CreatedBy_And_CareatedAt_In_Expense : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.AddColumn<DateTime>(
|
|
||||||
name: "CreatedAt",
|
|
||||||
table: "Expenses",
|
|
||||||
type: "datetime(6)",
|
|
||||||
nullable: false,
|
|
||||||
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
|
|
||||||
|
|
||||||
migrationBuilder.AddColumn<Guid>(
|
|
||||||
name: "CreatedById",
|
|
||||||
table: "Expenses",
|
|
||||||
type: "char(36)",
|
|
||||||
nullable: false,
|
|
||||||
defaultValue: new Guid("00000000-0000-0000-0000-000000000000"),
|
|
||||||
collation: "ascii_general_ci");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Expenses_CreatedById",
|
|
||||||
table: "Expenses",
|
|
||||||
column: "CreatedById");
|
|
||||||
|
|
||||||
migrationBuilder.AddForeignKey(
|
|
||||||
name: "FK_Expenses_Employees_CreatedById",
|
|
||||||
table: "Expenses",
|
|
||||||
column: "CreatedById",
|
|
||||||
principalTable: "Employees",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropForeignKey(
|
|
||||||
name: "FK_Expenses_Employees_CreatedById",
|
|
||||||
table: "Expenses");
|
|
||||||
|
|
||||||
migrationBuilder.DropIndex(
|
|
||||||
name: "IX_Expenses_CreatedById",
|
|
||||||
table: "Expenses");
|
|
||||||
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "CreatedAt",
|
|
||||||
table: "Expenses");
|
|
||||||
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "CreatedById",
|
|
||||||
table: "Expenses");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
@ -1,62 +0,0 @@
|
|||||||
using System;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Marco.Pms.DataAccess.Migrations
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class Added_ExpenseLog_Table : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "ExpenseLogs",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
|
||||||
ExpenseId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
|
||||||
UpdatedById = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
|
||||||
Action = table.Column<string>(type: "longtext", nullable: false)
|
|
||||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
|
||||||
Comment = table.Column<string>(type: "longtext", nullable: true)
|
|
||||||
.Annotation("MySql:CharSet", "utf8mb4")
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_ExpenseLogs", x => x.Id);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_ExpenseLogs_Employees_UpdatedById",
|
|
||||||
column: x => x.UpdatedById,
|
|
||||||
principalTable: "Employees",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_ExpenseLogs_Expenses_ExpenseId",
|
|
||||||
column: x => x.ExpenseId,
|
|
||||||
principalTable: "Expenses",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
})
|
|
||||||
.Annotation("MySql:CharSet", "utf8mb4");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_ExpenseLogs_ExpenseId",
|
|
||||||
table: "ExpenseLogs",
|
|
||||||
column: "ExpenseId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_ExpenseLogs_UpdatedById",
|
|
||||||
table: "ExpenseLogs",
|
|
||||||
column: "UpdatedById");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "ExpenseLogs");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,149 +0,0 @@
|
|||||||
using System;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional
|
|
||||||
|
|
||||||
namespace Marco.Pms.DataAccess.Migrations
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class Added_ExpensesStatusMaping_Table : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "StatusMapping");
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "ExpensesStatusMapping",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
|
||||||
StatusId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
|
||||||
ExpeStatusIdnsesId = table.Column<Guid>(type: "char(36)", nullable: true, collation: "ascii_general_ci"),
|
|
||||||
NextStatusId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
|
||||||
TenantId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci")
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_ExpensesStatusMapping", x => x.Id);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_ExpensesStatusMapping_ExpensesStatusMaster_ExpeStatusIdnsesId",
|
|
||||||
column: x => x.ExpeStatusIdnsesId,
|
|
||||||
principalTable: "ExpensesStatusMaster",
|
|
||||||
principalColumn: "Id");
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_ExpensesStatusMapping_ExpensesStatusMaster_NextStatusId",
|
|
||||||
column: x => x.NextStatusId,
|
|
||||||
principalTable: "ExpensesStatusMaster",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_ExpensesStatusMapping_Tenants_TenantId",
|
|
||||||
column: x => x.TenantId,
|
|
||||||
principalTable: "Tenants",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
})
|
|
||||||
.Annotation("MySql:CharSet", "utf8mb4");
|
|
||||||
|
|
||||||
migrationBuilder.InsertData(
|
|
||||||
table: "ExpensesStatusMapping",
|
|
||||||
columns: new[] { "Id", "ExpeStatusIdnsesId", "NextStatusId", "StatusId", "TenantId" },
|
|
||||||
values: new object[,]
|
|
||||||
{
|
|
||||||
{ new Guid("1fca1700-1266-477d-bba4-9ac3753aa33c"), null, new Guid("f18c5cfd-7815-4341-8da2-2c2d65778e27"), new Guid("4068007f-c92f-4f37-a907-bc15fe57d4d8"), new Guid("b3466e83-7e11-464c-b93a-daf047838b26") },
|
|
||||||
{ new Guid("36c00548-241c-43ec-bc95-cacebedb925c"), null, new Guid("d1ee5eec-24b6-4364-8673-a8f859c60729"), new Guid("4068007f-c92f-4f37-a907-bc15fe57d4d8"), new Guid("b3466e83-7e11-464c-b93a-daf047838b26") },
|
|
||||||
{ new Guid("5cf7f1df-9d1f-4289-add0-1775ad614f25"), null, new Guid("f18c5cfd-7815-4341-8da2-2c2d65778e27"), new Guid("f18c5cfd-7815-4341-8da2-2c2d65778e27"), new Guid("b3466e83-7e11-464c-b93a-daf047838b26") },
|
|
||||||
{ new Guid("af1e4492-98ee-4451-8ab7-fd8323f29c32"), null, new Guid("6537018f-f4e9-4cb3-a210-6c3b2da999d7"), new Guid("297e0d8f-f668-41b5-bfea-e03b354251c8"), new Guid("b3466e83-7e11-464c-b93a-daf047838b26") },
|
|
||||||
{ new Guid("ef1fcfbc-60e0-4f17-9308-c583a05d48fd"), null, new Guid("4068007f-c92f-4f37-a907-bc15fe57d4d8"), new Guid("6537018f-f4e9-4cb3-a210-6c3b2da999d7"), new Guid("b3466e83-7e11-464c-b93a-daf047838b26") },
|
|
||||||
{ new Guid("fddaaf20-4ccc-4f4e-a724-dd310272b356"), null, new Guid("d1ee5eec-24b6-4364-8673-a8f859c60729"), new Guid("6537018f-f4e9-4cb3-a210-6c3b2da999d7"), new Guid("b3466e83-7e11-464c-b93a-daf047838b26") }
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_ExpensesStatusMapping_ExpeStatusIdnsesId",
|
|
||||||
table: "ExpensesStatusMapping",
|
|
||||||
column: "ExpeStatusIdnsesId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_ExpensesStatusMapping_NextStatusId",
|
|
||||||
table: "ExpensesStatusMapping",
|
|
||||||
column: "NextStatusId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_ExpensesStatusMapping_TenantId",
|
|
||||||
table: "ExpensesStatusMapping",
|
|
||||||
column: "TenantId");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "ExpensesStatusMapping");
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "StatusMapping",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
|
||||||
ExpeStatusIdnsesId = table.Column<Guid>(type: "char(36)", nullable: true, collation: "ascii_general_ci"),
|
|
||||||
NextStatusId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
|
||||||
TenantId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
|
||||||
StatusId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci")
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_StatusMapping", x => x.Id);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_StatusMapping_ExpensesStatusMaster_ExpeStatusIdnsesId",
|
|
||||||
column: x => x.ExpeStatusIdnsesId,
|
|
||||||
principalTable: "ExpensesStatusMaster",
|
|
||||||
principalColumn: "Id");
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_StatusMapping_ExpensesStatusMaster_NextStatusId",
|
|
||||||
column: x => x.NextStatusId,
|
|
||||||
principalTable: "ExpensesStatusMaster",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_StatusMapping_Tenants_TenantId",
|
|
||||||
column: x => x.TenantId,
|
|
||||||
principalTable: "Tenants",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
})
|
|
||||||
.Annotation("MySql:CharSet", "utf8mb4");
|
|
||||||
|
|
||||||
migrationBuilder.InsertData(
|
|
||||||
table: "StatusMapping",
|
|
||||||
columns: new[] { "Id", "ExpeStatusIdnsesId", "NextStatusId", "StatusId", "TenantId" },
|
|
||||||
values: new object[,]
|
|
||||||
{
|
|
||||||
{ new Guid("1fca1700-1266-477d-bba4-9ac3753aa33c"), null, new Guid("f18c5cfd-7815-4341-8da2-2c2d65778e27"), new Guid("4068007f-c92f-4f37-a907-bc15fe57d4d8"), new Guid("b3466e83-7e11-464c-b93a-daf047838b26") },
|
|
||||||
{ new Guid("36c00548-241c-43ec-bc95-cacebedb925c"), null, new Guid("d1ee5eec-24b6-4364-8673-a8f859c60729"), new Guid("4068007f-c92f-4f37-a907-bc15fe57d4d8"), new Guid("b3466e83-7e11-464c-b93a-daf047838b26") },
|
|
||||||
{ new Guid("5cf7f1df-9d1f-4289-add0-1775ad614f25"), null, new Guid("f18c5cfd-7815-4341-8da2-2c2d65778e27"), new Guid("f18c5cfd-7815-4341-8da2-2c2d65778e27"), new Guid("b3466e83-7e11-464c-b93a-daf047838b26") },
|
|
||||||
{ new Guid("af1e4492-98ee-4451-8ab7-fd8323f29c32"), null, new Guid("6537018f-f4e9-4cb3-a210-6c3b2da999d7"), new Guid("297e0d8f-f668-41b5-bfea-e03b354251c8"), new Guid("b3466e83-7e11-464c-b93a-daf047838b26") },
|
|
||||||
{ new Guid("ef1fcfbc-60e0-4f17-9308-c583a05d48fd"), null, new Guid("4068007f-c92f-4f37-a907-bc15fe57d4d8"), new Guid("6537018f-f4e9-4cb3-a210-6c3b2da999d7"), new Guid("b3466e83-7e11-464c-b93a-daf047838b26") },
|
|
||||||
{ new Guid("fddaaf20-4ccc-4f4e-a724-dd310272b356"), null, new Guid("d1ee5eec-24b6-4364-8673-a8f859c60729"), new Guid("6537018f-f4e9-4cb3-a210-6c3b2da999d7"), new Guid("b3466e83-7e11-464c-b93a-daf047838b26") }
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_StatusMapping_ExpeStatusIdnsesId",
|
|
||||||
table: "StatusMapping",
|
|
||||||
column: "ExpeStatusIdnsesId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_StatusMapping_NextStatusId",
|
|
||||||
table: "StatusMapping",
|
|
||||||
column: "NextStatusId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_StatusMapping_TenantId",
|
|
||||||
table: "StatusMapping",
|
|
||||||
column: "TenantId");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -12,8 +12,8 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|||||||
namespace Marco.Pms.DataAccess.Migrations
|
namespace Marco.Pms.DataAccess.Migrations
|
||||||
{
|
{
|
||||||
[DbContext(typeof(ApplicationDbContext))]
|
[DbContext(typeof(ApplicationDbContext))]
|
||||||
[Migration("20250719113715_Added_ExpensesStatusMaping_Table")]
|
[Migration("20250721124928_Added_Expense_Related_Tables")]
|
||||||
partial class Added_ExpensesStatusMaping_Table
|
partial class Added_Expense_Related_Tables
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
@ -1307,6 +1307,9 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
b.Property<Guid>("ExpenseId")
|
b.Property<Guid>("ExpenseId")
|
||||||
.HasColumnType("char(36)");
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<Guid>("TenantId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
b.Property<Guid>("UpdatedById")
|
b.Property<Guid>("UpdatedById")
|
||||||
.HasColumnType("char(36)");
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
@ -1314,6 +1317,8 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
|
|
||||||
b.HasIndex("ExpenseId");
|
b.HasIndex("ExpenseId");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId");
|
||||||
|
|
||||||
b.HasIndex("UpdatedById");
|
b.HasIndex("UpdatedById");
|
||||||
|
|
||||||
b.ToTable("ExpenseLogs");
|
b.ToTable("ExpenseLogs");
|
||||||
@ -1444,12 +1449,17 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
b.Property<Guid>("ExpensesReimburseId")
|
b.Property<Guid>("ExpensesReimburseId")
|
||||||
.HasColumnType("char(36)");
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<Guid>("TenantId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("ExpensesId");
|
b.HasIndex("ExpensesId");
|
||||||
|
|
||||||
b.HasIndex("ExpensesReimburseId");
|
b.HasIndex("ExpensesReimburseId");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId");
|
||||||
|
|
||||||
b.ToTable("ExpensesReimburseMapping");
|
b.ToTable("ExpensesReimburseMapping");
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1459,9 +1469,6 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("char(36)");
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
b.Property<Guid?>("ExpeStatusIdnsesId")
|
|
||||||
.HasColumnType("char(36)");
|
|
||||||
|
|
||||||
b.Property<Guid>("NextStatusId")
|
b.Property<Guid>("NextStatusId")
|
||||||
.HasColumnType("char(36)");
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
@ -1473,10 +1480,10 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("ExpeStatusIdnsesId");
|
|
||||||
|
|
||||||
b.HasIndex("NextStatusId");
|
b.HasIndex("NextStatusId");
|
||||||
|
|
||||||
|
b.HasIndex("StatusId");
|
||||||
|
|
||||||
b.HasIndex("TenantId");
|
b.HasIndex("TenantId");
|
||||||
|
|
||||||
b.ToTable("ExpensesStatusMapping");
|
b.ToTable("ExpensesStatusMapping");
|
||||||
@ -1485,7 +1492,7 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = new Guid("5cf7f1df-9d1f-4289-add0-1775ad614f25"),
|
Id = new Guid("5cf7f1df-9d1f-4289-add0-1775ad614f25"),
|
||||||
NextStatusId = new Guid("f18c5cfd-7815-4341-8da2-2c2d65778e27"),
|
NextStatusId = new Guid("61578360-3a49-4c34-8604-7b35a3787b95"),
|
||||||
StatusId = new Guid("f18c5cfd-7815-4341-8da2-2c2d65778e27"),
|
StatusId = new Guid("f18c5cfd-7815-4341-8da2-2c2d65778e27"),
|
||||||
TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26")
|
TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26")
|
||||||
},
|
},
|
||||||
@ -1504,6 +1511,13 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26")
|
TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26")
|
||||||
},
|
},
|
||||||
new
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("75bbda6a-6a53-47d1-ad71-5f5f9446a11e"),
|
||||||
|
NextStatusId = new Guid("6537018f-f4e9-4cb3-a210-6c3b2da999d7"),
|
||||||
|
StatusId = new Guid("d1ee5eec-24b6-4364-8673-a8f859c60729"),
|
||||||
|
TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26")
|
||||||
|
},
|
||||||
|
new
|
||||||
{
|
{
|
||||||
Id = new Guid("fddaaf20-4ccc-4f4e-a724-dd310272b356"),
|
Id = new Guid("fddaaf20-4ccc-4f4e-a724-dd310272b356"),
|
||||||
NextStatusId = new Guid("d1ee5eec-24b6-4364-8673-a8f859c60729"),
|
NextStatusId = new Guid("d1ee5eec-24b6-4364-8673-a8f859c60729"),
|
||||||
@ -1538,13 +1552,55 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
b.Property<Guid>("StatusId")
|
b.Property<Guid>("StatusId")
|
||||||
.HasColumnType("char(36)");
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<Guid>("TenantId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("PermissionId");
|
b.HasIndex("PermissionId");
|
||||||
|
|
||||||
b.HasIndex("StatusId");
|
b.HasIndex("StatusId");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId");
|
||||||
|
|
||||||
b.ToTable("StatusPermissionMapping");
|
b.ToTable("StatusPermissionMapping");
|
||||||
|
|
||||||
|
b.HasData(
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("ed893799-1a5f-4311-a077-de93c86ca8fd"),
|
||||||
|
PermissionId = new Guid("1f4bda08-1873-449a-bb66-3e8222bd871b"),
|
||||||
|
StatusId = new Guid("4068007f-c92f-4f37-a907-bc15fe57d4d8"),
|
||||||
|
TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26")
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("4652d73f-fc71-4fe1-9f2f-1e48b342d741"),
|
||||||
|
PermissionId = new Guid("1f4bda08-1873-449a-bb66-3e8222bd871b"),
|
||||||
|
StatusId = new Guid("d1ee5eec-24b6-4364-8673-a8f859c60729"),
|
||||||
|
TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26")
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("cd15f9b9-be45-4deb-9c71-2f23f872dbcd"),
|
||||||
|
PermissionId = new Guid("eaafdd76-8aac-45f9-a530-315589c6deca"),
|
||||||
|
StatusId = new Guid("d1ee5eec-24b6-4364-8673-a8f859c60729"),
|
||||||
|
TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26")
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("f6f26b2f-2fa6-40b7-8601-cbd4bcdda0cc"),
|
||||||
|
PermissionId = new Guid("eaafdd76-8aac-45f9-a530-315589c6deca"),
|
||||||
|
StatusId = new Guid("f18c5cfd-7815-4341-8da2-2c2d65778e27"),
|
||||||
|
TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26")
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("214354e5-daad-4569-ad69-eb5bf4e87fbc"),
|
||||||
|
PermissionId = new Guid("ea5a1529-4ee8-4828-80ea-0e23c9d4dd11"),
|
||||||
|
StatusId = new Guid("61578360-3a49-4c34-8604-7b35a3787b95"),
|
||||||
|
TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26")
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Marco.Pms.Model.Forum.TicketAttachment", b =>
|
modelBuilder.Entity("Marco.Pms.Model.Forum.TicketAttachment", b =>
|
||||||
@ -1843,10 +1899,18 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("char(36)");
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<string>("Color")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
b.Property<string>("Description")
|
b.Property<string>("Description")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("longtext");
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<string>("DisplayName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
b.Property<bool>("IsActive")
|
b.Property<bool>("IsActive")
|
||||||
.HasColumnType("tinyint(1)");
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
@ -1870,7 +1934,9 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = new Guid("297e0d8f-f668-41b5-bfea-e03b354251c8"),
|
Id = new Guid("297e0d8f-f668-41b5-bfea-e03b354251c8"),
|
||||||
|
Color = "#212529",
|
||||||
Description = "Expense has been created but not yet submitted.",
|
Description = "Expense has been created but not yet submitted.",
|
||||||
|
DisplayName = "Draft",
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
IsSystem = true,
|
IsSystem = true,
|
||||||
Name = "Draft",
|
Name = "Draft",
|
||||||
@ -1879,7 +1945,9 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = new Guid("6537018f-f4e9-4cb3-a210-6c3b2da999d7"),
|
Id = new Guid("6537018f-f4e9-4cb3-a210-6c3b2da999d7"),
|
||||||
|
Color = "#0d6efd",
|
||||||
Description = "Reviewer is currently reviewing the expense.",
|
Description = "Reviewer is currently reviewing the expense.",
|
||||||
|
DisplayName = "Review",
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
IsSystem = true,
|
IsSystem = true,
|
||||||
Name = "Review Pending",
|
Name = "Review Pending",
|
||||||
@ -1888,7 +1956,9 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = new Guid("4068007f-c92f-4f37-a907-bc15fe57d4d8"),
|
Id = new Guid("4068007f-c92f-4f37-a907-bc15fe57d4d8"),
|
||||||
|
Color = "#0dcaf0",
|
||||||
Description = "Review is completed, waiting for action of approver.",
|
Description = "Review is completed, waiting for action of approver.",
|
||||||
|
DisplayName = "Approve",
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
IsSystem = true,
|
IsSystem = true,
|
||||||
Name = "Approval Pending",
|
Name = "Approval Pending",
|
||||||
@ -1897,7 +1967,9 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = new Guid("d1ee5eec-24b6-4364-8673-a8f859c60729"),
|
Id = new Guid("d1ee5eec-24b6-4364-8673-a8f859c60729"),
|
||||||
|
Color = "#dc3545",
|
||||||
Description = "Expense was declined, often with a reason(either review rejected or approval rejected.",
|
Description = "Expense was declined, often with a reason(either review rejected or approval rejected.",
|
||||||
|
DisplayName = "Reject",
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
IsSystem = true,
|
IsSystem = true,
|
||||||
Name = "Rejected",
|
Name = "Rejected",
|
||||||
@ -1906,7 +1978,9 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = new Guid("f18c5cfd-7815-4341-8da2-2c2d65778e27"),
|
Id = new Guid("f18c5cfd-7815-4341-8da2-2c2d65778e27"),
|
||||||
|
Color = "#ffc107",
|
||||||
Description = "Approved expense is awaiting final payment.",
|
Description = "Approved expense is awaiting final payment.",
|
||||||
|
DisplayName = "Process",
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
IsSystem = true,
|
IsSystem = true,
|
||||||
Name = "Process Pending",
|
Name = "Process Pending",
|
||||||
@ -1915,7 +1989,9 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = new Guid("61578360-3a49-4c34-8604-7b35a3787b95"),
|
Id = new Guid("61578360-3a49-4c34-8604-7b35a3787b95"),
|
||||||
|
Color = "#198754",
|
||||||
Description = "Expense has been settled.",
|
Description = "Expense has been settled.",
|
||||||
|
DisplayName = "Paid",
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
IsSystem = true,
|
IsSystem = true,
|
||||||
Name = "Processed",
|
Name = "Processed",
|
||||||
@ -3696,6 +3772,12 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("TenantId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
b.HasOne("Marco.Pms.Model.Employees.Employee", "UpdatedBy")
|
b.HasOne("Marco.Pms.Model.Employees.Employee", "UpdatedBy")
|
||||||
.WithMany()
|
.WithMany()
|
||||||
.HasForeignKey("UpdatedById")
|
.HasForeignKey("UpdatedById")
|
||||||
@ -3704,6 +3786,8 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
|
|
||||||
b.Navigation("Expense");
|
b.Navigation("Expense");
|
||||||
|
|
||||||
|
b.Navigation("Tenant");
|
||||||
|
|
||||||
b.Navigation("UpdatedBy");
|
b.Navigation("UpdatedBy");
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -3799,23 +3883,33 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("TenantId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
b.Navigation("Expenses");
|
b.Navigation("Expenses");
|
||||||
|
|
||||||
b.Navigation("ExpensesReimburse");
|
b.Navigation("ExpensesReimburse");
|
||||||
|
|
||||||
|
b.Navigation("Tenant");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Marco.Pms.Model.Expenses.ExpensesStatusMapping", b =>
|
modelBuilder.Entity("Marco.Pms.Model.Expenses.ExpensesStatusMapping", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Marco.Pms.Model.Master.ExpensesStatusMaster", "Status")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("ExpeStatusIdnsesId");
|
|
||||||
|
|
||||||
b.HasOne("Marco.Pms.Model.Master.ExpensesStatusMaster", "NextStatus")
|
b.HasOne("Marco.Pms.Model.Master.ExpensesStatusMaster", "NextStatus")
|
||||||
.WithMany()
|
.WithMany()
|
||||||
.HasForeignKey("NextStatusId")
|
.HasForeignKey("NextStatusId")
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Marco.Pms.Model.Master.ExpensesStatusMaster", "Status")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("StatusId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant")
|
b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant")
|
||||||
.WithMany()
|
.WithMany()
|
||||||
.HasForeignKey("TenantId")
|
.HasForeignKey("TenantId")
|
||||||
@ -3843,9 +3937,17 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("TenantId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
b.Navigation("Permission");
|
b.Navigation("Permission");
|
||||||
|
|
||||||
b.Navigation("Status");
|
b.Navigation("Status");
|
||||||
|
|
||||||
|
b.Navigation("Tenant");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Marco.Pms.Model.Forum.TicketAttachment", b =>
|
modelBuilder.Entity("Marco.Pms.Model.Forum.TicketAttachment", b =>
|
@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Migrations;
|
|||||||
namespace Marco.Pms.DataAccess.Migrations
|
namespace Marco.Pms.DataAccess.Migrations
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public partial class Expenses_tables_Added : Migration
|
public partial class Added_Expense_Related_Tables : Migration
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
@ -51,8 +51,12 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
Name = table.Column<string>(type: "longtext", nullable: false)
|
Name = table.Column<string>(type: "longtext", nullable: false)
|
||||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
DisplayName = table.Column<string>(type: "longtext", nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
Description = table.Column<string>(type: "longtext", nullable: false)
|
Description = table.Column<string>(type: "longtext", nullable: false)
|
||||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
Color = table.Column<string>(type: "longtext", nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
IsSystem = table.Column<bool>(type: "tinyint(1)", nullable: false),
|
IsSystem = table.Column<bool>(type: "tinyint(1)", nullable: false),
|
||||||
IsActive = table.Column<bool>(type: "tinyint(1)", nullable: false),
|
IsActive = table.Column<bool>(type: "tinyint(1)", nullable: false),
|
||||||
TenantId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci")
|
TenantId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci")
|
||||||
@ -119,31 +123,31 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
.Annotation("MySql:CharSet", "utf8mb4");
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
migrationBuilder.CreateTable(
|
||||||
name: "StatusMapping",
|
name: "ExpensesStatusMapping",
|
||||||
columns: table => new
|
columns: table => new
|
||||||
{
|
{
|
||||||
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
StatusId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
StatusId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
ExpeStatusIdnsesId = table.Column<Guid>(type: "char(36)", nullable: true, collation: "ascii_general_ci"),
|
|
||||||
NextStatusId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
NextStatusId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
TenantId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci")
|
TenantId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci")
|
||||||
},
|
},
|
||||||
constraints: table =>
|
constraints: table =>
|
||||||
{
|
{
|
||||||
table.PrimaryKey("PK_StatusMapping", x => x.Id);
|
table.PrimaryKey("PK_ExpensesStatusMapping", x => x.Id);
|
||||||
table.ForeignKey(
|
table.ForeignKey(
|
||||||
name: "FK_StatusMapping_ExpensesStatusMaster_ExpeStatusIdnsesId",
|
name: "FK_ExpensesStatusMapping_ExpensesStatusMaster_NextStatusId",
|
||||||
column: x => x.ExpeStatusIdnsesId,
|
|
||||||
principalTable: "ExpensesStatusMaster",
|
|
||||||
principalColumn: "Id");
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_StatusMapping_ExpensesStatusMaster_NextStatusId",
|
|
||||||
column: x => x.NextStatusId,
|
column: x => x.NextStatusId,
|
||||||
principalTable: "ExpensesStatusMaster",
|
principalTable: "ExpensesStatusMaster",
|
||||||
principalColumn: "Id",
|
principalColumn: "Id",
|
||||||
onDelete: ReferentialAction.Cascade);
|
onDelete: ReferentialAction.Cascade);
|
||||||
table.ForeignKey(
|
table.ForeignKey(
|
||||||
name: "FK_StatusMapping_Tenants_TenantId",
|
name: "FK_ExpensesStatusMapping_ExpensesStatusMaster_StatusId",
|
||||||
|
column: x => x.StatusId,
|
||||||
|
principalTable: "ExpensesStatusMaster",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_ExpensesStatusMapping_Tenants_TenantId",
|
||||||
column: x => x.TenantId,
|
column: x => x.TenantId,
|
||||||
principalTable: "Tenants",
|
principalTable: "Tenants",
|
||||||
principalColumn: "Id",
|
principalColumn: "Id",
|
||||||
@ -157,7 +161,8 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
{
|
{
|
||||||
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
StatusId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
StatusId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
PermissionId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci")
|
PermissionId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
|
TenantId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci")
|
||||||
},
|
},
|
||||||
constraints: table =>
|
constraints: table =>
|
||||||
{
|
{
|
||||||
@ -174,6 +179,12 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
principalTable: "FeaturePermissions",
|
principalTable: "FeaturePermissions",
|
||||||
principalColumn: "Id",
|
principalColumn: "Id",
|
||||||
onDelete: ReferentialAction.Cascade);
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_StatusPermissionMapping_Tenants_TenantId",
|
||||||
|
column: x => x.TenantId,
|
||||||
|
principalTable: "Tenants",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
})
|
})
|
||||||
.Annotation("MySql:CharSet", "utf8mb4");
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
@ -186,7 +197,9 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
ExpensesTypeId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
ExpensesTypeId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
PaymentModeId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
PaymentModeId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
PaidById = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
PaidById = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
|
CreatedById = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
TransactionDate = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
TransactionDate = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||||
TransactionId = table.Column<string>(type: "longtext", nullable: true)
|
TransactionId = table.Column<string>(type: "longtext", nullable: true)
|
||||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
Description = table.Column<string>(type: "longtext", nullable: false)
|
Description = table.Column<string>(type: "longtext", nullable: false)
|
||||||
@ -207,6 +220,12 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
constraints: table =>
|
constraints: table =>
|
||||||
{
|
{
|
||||||
table.PrimaryKey("PK_Expenses", x => x.Id);
|
table.PrimaryKey("PK_Expenses", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Expenses_Employees_CreatedById",
|
||||||
|
column: x => x.CreatedById,
|
||||||
|
principalTable: "Employees",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
table.ForeignKey(
|
table.ForeignKey(
|
||||||
name: "FK_Expenses_Employees_PaidById",
|
name: "FK_Expenses_Employees_PaidById",
|
||||||
column: x => x.PaidById,
|
column: x => x.PaidById,
|
||||||
@ -279,13 +298,51 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
})
|
})
|
||||||
.Annotation("MySql:CharSet", "utf8mb4");
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "ExpenseLogs",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
|
ExpenseId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
|
UpdatedById = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
|
Action = table.Column<string>(type: "longtext", nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
Comment = table.Column<string>(type: "longtext", nullable: true)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
TenantId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_ExpenseLogs", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_ExpenseLogs_Employees_UpdatedById",
|
||||||
|
column: x => x.UpdatedById,
|
||||||
|
principalTable: "Employees",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_ExpenseLogs_Expenses_ExpenseId",
|
||||||
|
column: x => x.ExpenseId,
|
||||||
|
principalTable: "Expenses",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_ExpenseLogs_Tenants_TenantId",
|
||||||
|
column: x => x.TenantId,
|
||||||
|
principalTable: "Tenants",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
})
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
migrationBuilder.CreateTable(
|
||||||
name: "ExpensesReimburseMapping",
|
name: "ExpensesReimburseMapping",
|
||||||
columns: table => new
|
columns: table => new
|
||||||
{
|
{
|
||||||
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
ExpensesId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
ExpensesId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
ExpensesReimburseId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci")
|
ExpensesReimburseId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
|
TenantId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci")
|
||||||
},
|
},
|
||||||
constraints: table =>
|
constraints: table =>
|
||||||
{
|
{
|
||||||
@ -302,20 +359,26 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
principalTable: "Expenses",
|
principalTable: "Expenses",
|
||||||
principalColumn: "Id",
|
principalColumn: "Id",
|
||||||
onDelete: ReferentialAction.Cascade);
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_ExpensesReimburseMapping_Tenants_TenantId",
|
||||||
|
column: x => x.TenantId,
|
||||||
|
principalTable: "Tenants",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
})
|
})
|
||||||
.Annotation("MySql:CharSet", "utf8mb4");
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
migrationBuilder.InsertData(
|
migrationBuilder.InsertData(
|
||||||
table: "ExpensesStatusMaster",
|
table: "ExpensesStatusMaster",
|
||||||
columns: new[] { "Id", "Description", "IsActive", "IsSystem", "Name", "TenantId" },
|
columns: new[] { "Id", "Color", "Description", "DisplayName", "IsActive", "IsSystem", "Name", "TenantId" },
|
||||||
values: new object[,]
|
values: new object[,]
|
||||||
{
|
{
|
||||||
{ new Guid("297e0d8f-f668-41b5-bfea-e03b354251c8"), "Expense has been created but not yet submitted.", true, true, "Draft", new Guid("b3466e83-7e11-464c-b93a-daf047838b26") },
|
{ new Guid("297e0d8f-f668-41b5-bfea-e03b354251c8"), "#212529", "Expense has been created but not yet submitted.", "Draft", true, true, "Draft", new Guid("b3466e83-7e11-464c-b93a-daf047838b26") },
|
||||||
{ new Guid("4068007f-c92f-4f37-a907-bc15fe57d4d8"), "Review is completed, waiting for action of approver.", true, true, "Approval Pending", new Guid("b3466e83-7e11-464c-b93a-daf047838b26") },
|
{ new Guid("4068007f-c92f-4f37-a907-bc15fe57d4d8"), "#0dcaf0", "Review is completed, waiting for action of approver.", "Approve", true, true, "Approval Pending", new Guid("b3466e83-7e11-464c-b93a-daf047838b26") },
|
||||||
{ new Guid("61578360-3a49-4c34-8604-7b35a3787b95"), "Expense has been settled.", true, true, "Processed", new Guid("b3466e83-7e11-464c-b93a-daf047838b26") },
|
{ new Guid("61578360-3a49-4c34-8604-7b35a3787b95"), "#198754", "Expense has been settled.", "Paid", true, true, "Processed", new Guid("b3466e83-7e11-464c-b93a-daf047838b26") },
|
||||||
{ new Guid("6537018f-f4e9-4cb3-a210-6c3b2da999d7"), "Reviewer is currently reviewing the expense.", true, true, "Review Pending", new Guid("b3466e83-7e11-464c-b93a-daf047838b26") },
|
{ new Guid("6537018f-f4e9-4cb3-a210-6c3b2da999d7"), "#0d6efd", "Reviewer is currently reviewing the expense.", "Review", true, true, "Review Pending", new Guid("b3466e83-7e11-464c-b93a-daf047838b26") },
|
||||||
{ new Guid("d1ee5eec-24b6-4364-8673-a8f859c60729"), "Expense was declined, often with a reason(either review rejected or approval rejected.", true, true, "Rejected", new Guid("b3466e83-7e11-464c-b93a-daf047838b26") },
|
{ new Guid("d1ee5eec-24b6-4364-8673-a8f859c60729"), "#dc3545", "Expense was declined, often with a reason(either review rejected or approval rejected.", "Reject", true, true, "Rejected", new Guid("b3466e83-7e11-464c-b93a-daf047838b26") },
|
||||||
{ new Guid("f18c5cfd-7815-4341-8da2-2c2d65778e27"), "Approved expense is awaiting final payment.", true, true, "Process Pending", new Guid("b3466e83-7e11-464c-b93a-daf047838b26") }
|
{ new Guid("f18c5cfd-7815-4341-8da2-2c2d65778e27"), "#ffc107", "Approved expense is awaiting final payment.", "Process", true, true, "Process Pending", new Guid("b3466e83-7e11-464c-b93a-daf047838b26") }
|
||||||
});
|
});
|
||||||
|
|
||||||
migrationBuilder.InsertData(
|
migrationBuilder.InsertData(
|
||||||
@ -349,6 +412,20 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
{ new Guid("ed667353-8eea-4fd1-8750-719405932480"), "Online banking portals used to transfer funds directly between accounts", true, "NetBanking", new Guid("b3466e83-7e11-464c-b93a-daf047838b26") }
|
{ new Guid("ed667353-8eea-4fd1-8750-719405932480"), "Online banking portals used to transfer funds directly between accounts", true, "NetBanking", new Guid("b3466e83-7e11-464c-b93a-daf047838b26") }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
migrationBuilder.InsertData(
|
||||||
|
table: "ExpensesStatusMapping",
|
||||||
|
columns: new[] { "Id", "NextStatusId", "StatusId", "TenantId" },
|
||||||
|
values: new object[,]
|
||||||
|
{
|
||||||
|
{ new Guid("1fca1700-1266-477d-bba4-9ac3753aa33c"), new Guid("f18c5cfd-7815-4341-8da2-2c2d65778e27"), new Guid("4068007f-c92f-4f37-a907-bc15fe57d4d8"), new Guid("b3466e83-7e11-464c-b93a-daf047838b26") },
|
||||||
|
{ new Guid("36c00548-241c-43ec-bc95-cacebedb925c"), new Guid("d1ee5eec-24b6-4364-8673-a8f859c60729"), new Guid("4068007f-c92f-4f37-a907-bc15fe57d4d8"), new Guid("b3466e83-7e11-464c-b93a-daf047838b26") },
|
||||||
|
{ new Guid("5cf7f1df-9d1f-4289-add0-1775ad614f25"), new Guid("61578360-3a49-4c34-8604-7b35a3787b95"), new Guid("f18c5cfd-7815-4341-8da2-2c2d65778e27"), new Guid("b3466e83-7e11-464c-b93a-daf047838b26") },
|
||||||
|
{ new Guid("75bbda6a-6a53-47d1-ad71-5f5f9446a11e"), new Guid("6537018f-f4e9-4cb3-a210-6c3b2da999d7"), new Guid("d1ee5eec-24b6-4364-8673-a8f859c60729"), new Guid("b3466e83-7e11-464c-b93a-daf047838b26") },
|
||||||
|
{ new Guid("af1e4492-98ee-4451-8ab7-fd8323f29c32"), new Guid("6537018f-f4e9-4cb3-a210-6c3b2da999d7"), new Guid("297e0d8f-f668-41b5-bfea-e03b354251c8"), new Guid("b3466e83-7e11-464c-b93a-daf047838b26") },
|
||||||
|
{ new Guid("ef1fcfbc-60e0-4f17-9308-c583a05d48fd"), new Guid("4068007f-c92f-4f37-a907-bc15fe57d4d8"), new Guid("6537018f-f4e9-4cb3-a210-6c3b2da999d7"), new Guid("b3466e83-7e11-464c-b93a-daf047838b26") },
|
||||||
|
{ new Guid("fddaaf20-4ccc-4f4e-a724-dd310272b356"), new Guid("d1ee5eec-24b6-4364-8673-a8f859c60729"), new Guid("6537018f-f4e9-4cb3-a210-6c3b2da999d7"), new Guid("b3466e83-7e11-464c-b93a-daf047838b26") }
|
||||||
|
});
|
||||||
|
|
||||||
migrationBuilder.InsertData(
|
migrationBuilder.InsertData(
|
||||||
table: "FeaturePermissions",
|
table: "FeaturePermissions",
|
||||||
columns: new[] { "Id", "Description", "FeatureId", "IsEnabled", "Name" },
|
columns: new[] { "Id", "Description", "FeatureId", "IsEnabled", "Name" },
|
||||||
@ -364,16 +441,15 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
});
|
});
|
||||||
|
|
||||||
migrationBuilder.InsertData(
|
migrationBuilder.InsertData(
|
||||||
table: "StatusMapping",
|
table: "StatusPermissionMapping",
|
||||||
columns: new[] { "Id", "ExpeStatusIdnsesId", "NextStatusId", "StatusId", "TenantId" },
|
columns: new[] { "Id", "PermissionId", "StatusId", "TenantId" },
|
||||||
values: new object[,]
|
values: new object[,]
|
||||||
{
|
{
|
||||||
{ new Guid("1fca1700-1266-477d-bba4-9ac3753aa33c"), null, new Guid("f18c5cfd-7815-4341-8da2-2c2d65778e27"), new Guid("4068007f-c92f-4f37-a907-bc15fe57d4d8"), new Guid("b3466e83-7e11-464c-b93a-daf047838b26") },
|
{ new Guid("214354e5-daad-4569-ad69-eb5bf4e87fbc"), new Guid("ea5a1529-4ee8-4828-80ea-0e23c9d4dd11"), new Guid("61578360-3a49-4c34-8604-7b35a3787b95"), new Guid("b3466e83-7e11-464c-b93a-daf047838b26") },
|
||||||
{ new Guid("36c00548-241c-43ec-bc95-cacebedb925c"), null, new Guid("d1ee5eec-24b6-4364-8673-a8f859c60729"), new Guid("4068007f-c92f-4f37-a907-bc15fe57d4d8"), new Guid("b3466e83-7e11-464c-b93a-daf047838b26") },
|
{ new Guid("4652d73f-fc71-4fe1-9f2f-1e48b342d741"), new Guid("1f4bda08-1873-449a-bb66-3e8222bd871b"), new Guid("d1ee5eec-24b6-4364-8673-a8f859c60729"), new Guid("b3466e83-7e11-464c-b93a-daf047838b26") },
|
||||||
{ new Guid("5cf7f1df-9d1f-4289-add0-1775ad614f25"), null, new Guid("f18c5cfd-7815-4341-8da2-2c2d65778e27"), new Guid("f18c5cfd-7815-4341-8da2-2c2d65778e27"), new Guid("b3466e83-7e11-464c-b93a-daf047838b26") },
|
{ new Guid("cd15f9b9-be45-4deb-9c71-2f23f872dbcd"), new Guid("eaafdd76-8aac-45f9-a530-315589c6deca"), new Guid("d1ee5eec-24b6-4364-8673-a8f859c60729"), new Guid("b3466e83-7e11-464c-b93a-daf047838b26") },
|
||||||
{ new Guid("af1e4492-98ee-4451-8ab7-fd8323f29c32"), null, new Guid("6537018f-f4e9-4cb3-a210-6c3b2da999d7"), new Guid("297e0d8f-f668-41b5-bfea-e03b354251c8"), new Guid("b3466e83-7e11-464c-b93a-daf047838b26") },
|
{ new Guid("ed893799-1a5f-4311-a077-de93c86ca8fd"), new Guid("1f4bda08-1873-449a-bb66-3e8222bd871b"), new Guid("4068007f-c92f-4f37-a907-bc15fe57d4d8"), new Guid("b3466e83-7e11-464c-b93a-daf047838b26") },
|
||||||
{ new Guid("ef1fcfbc-60e0-4f17-9308-c583a05d48fd"), null, new Guid("4068007f-c92f-4f37-a907-bc15fe57d4d8"), new Guid("6537018f-f4e9-4cb3-a210-6c3b2da999d7"), new Guid("b3466e83-7e11-464c-b93a-daf047838b26") },
|
{ new Guid("f6f26b2f-2fa6-40b7-8601-cbd4bcdda0cc"), new Guid("eaafdd76-8aac-45f9-a530-315589c6deca"), new Guid("f18c5cfd-7815-4341-8da2-2c2d65778e27"), new Guid("b3466e83-7e11-464c-b93a-daf047838b26") }
|
||||||
{ new Guid("fddaaf20-4ccc-4f4e-a724-dd310272b356"), null, new Guid("d1ee5eec-24b6-4364-8673-a8f859c60729"), new Guid("6537018f-f4e9-4cb3-a210-6c3b2da999d7"), new Guid("b3466e83-7e11-464c-b93a-daf047838b26") }
|
|
||||||
});
|
});
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
migrationBuilder.CreateIndex(
|
||||||
@ -391,6 +467,26 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
table: "BillAttachments",
|
table: "BillAttachments",
|
||||||
column: "TenantId");
|
column: "TenantId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ExpenseLogs_ExpenseId",
|
||||||
|
table: "ExpenseLogs",
|
||||||
|
column: "ExpenseId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ExpenseLogs_TenantId",
|
||||||
|
table: "ExpenseLogs",
|
||||||
|
column: "TenantId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ExpenseLogs_UpdatedById",
|
||||||
|
table: "ExpenseLogs",
|
||||||
|
column: "UpdatedById");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Expenses_CreatedById",
|
||||||
|
table: "Expenses",
|
||||||
|
column: "CreatedById");
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
migrationBuilder.CreateIndex(
|
||||||
name: "IX_Expenses_ExpensesTypeId",
|
name: "IX_Expenses_ExpensesTypeId",
|
||||||
table: "Expenses",
|
table: "Expenses",
|
||||||
@ -441,6 +537,26 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
table: "ExpensesReimburseMapping",
|
table: "ExpensesReimburseMapping",
|
||||||
column: "ExpensesReimburseId");
|
column: "ExpensesReimburseId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ExpensesReimburseMapping_TenantId",
|
||||||
|
table: "ExpensesReimburseMapping",
|
||||||
|
column: "TenantId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ExpensesStatusMapping_NextStatusId",
|
||||||
|
table: "ExpensesStatusMapping",
|
||||||
|
column: "NextStatusId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ExpensesStatusMapping_StatusId",
|
||||||
|
table: "ExpensesStatusMapping",
|
||||||
|
column: "StatusId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ExpensesStatusMapping_TenantId",
|
||||||
|
table: "ExpensesStatusMapping",
|
||||||
|
column: "TenantId");
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
migrationBuilder.CreateIndex(
|
||||||
name: "IX_ExpensesStatusMaster_TenantId",
|
name: "IX_ExpensesStatusMaster_TenantId",
|
||||||
table: "ExpensesStatusMaster",
|
table: "ExpensesStatusMaster",
|
||||||
@ -456,21 +572,6 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
table: "PaymentModeMatser",
|
table: "PaymentModeMatser",
|
||||||
column: "TenantId");
|
column: "TenantId");
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_StatusMapping_ExpeStatusIdnsesId",
|
|
||||||
table: "StatusMapping",
|
|
||||||
column: "ExpeStatusIdnsesId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_StatusMapping_NextStatusId",
|
|
||||||
table: "StatusMapping",
|
|
||||||
column: "NextStatusId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_StatusMapping_TenantId",
|
|
||||||
table: "StatusMapping",
|
|
||||||
column: "TenantId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
migrationBuilder.CreateIndex(
|
||||||
name: "IX_StatusPermissionMapping_PermissionId",
|
name: "IX_StatusPermissionMapping_PermissionId",
|
||||||
table: "StatusPermissionMapping",
|
table: "StatusPermissionMapping",
|
||||||
@ -480,6 +581,11 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
name: "IX_StatusPermissionMapping_StatusId",
|
name: "IX_StatusPermissionMapping_StatusId",
|
||||||
table: "StatusPermissionMapping",
|
table: "StatusPermissionMapping",
|
||||||
column: "StatusId");
|
column: "StatusId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_StatusPermissionMapping_TenantId",
|
||||||
|
table: "StatusPermissionMapping",
|
||||||
|
column: "TenantId");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@ -488,11 +594,14 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
migrationBuilder.DropTable(
|
migrationBuilder.DropTable(
|
||||||
name: "BillAttachments");
|
name: "BillAttachments");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "ExpenseLogs");
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
migrationBuilder.DropTable(
|
||||||
name: "ExpensesReimburseMapping");
|
name: "ExpensesReimburseMapping");
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
migrationBuilder.DropTable(
|
||||||
name: "StatusMapping");
|
name: "ExpensesStatusMapping");
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
migrationBuilder.DropTable(
|
||||||
name: "StatusPermissionMapping");
|
name: "StatusPermissionMapping");
|
@ -1304,6 +1304,9 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
b.Property<Guid>("ExpenseId")
|
b.Property<Guid>("ExpenseId")
|
||||||
.HasColumnType("char(36)");
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<Guid>("TenantId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
b.Property<Guid>("UpdatedById")
|
b.Property<Guid>("UpdatedById")
|
||||||
.HasColumnType("char(36)");
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
@ -1311,6 +1314,8 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
|
|
||||||
b.HasIndex("ExpenseId");
|
b.HasIndex("ExpenseId");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId");
|
||||||
|
|
||||||
b.HasIndex("UpdatedById");
|
b.HasIndex("UpdatedById");
|
||||||
|
|
||||||
b.ToTable("ExpenseLogs");
|
b.ToTable("ExpenseLogs");
|
||||||
@ -1441,12 +1446,17 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
b.Property<Guid>("ExpensesReimburseId")
|
b.Property<Guid>("ExpensesReimburseId")
|
||||||
.HasColumnType("char(36)");
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<Guid>("TenantId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("ExpensesId");
|
b.HasIndex("ExpensesId");
|
||||||
|
|
||||||
b.HasIndex("ExpensesReimburseId");
|
b.HasIndex("ExpensesReimburseId");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId");
|
||||||
|
|
||||||
b.ToTable("ExpensesReimburseMapping");
|
b.ToTable("ExpensesReimburseMapping");
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1456,9 +1466,6 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("char(36)");
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
b.Property<Guid?>("ExpeStatusIdnsesId")
|
|
||||||
.HasColumnType("char(36)");
|
|
||||||
|
|
||||||
b.Property<Guid>("NextStatusId")
|
b.Property<Guid>("NextStatusId")
|
||||||
.HasColumnType("char(36)");
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
@ -1470,10 +1477,10 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("ExpeStatusIdnsesId");
|
|
||||||
|
|
||||||
b.HasIndex("NextStatusId");
|
b.HasIndex("NextStatusId");
|
||||||
|
|
||||||
|
b.HasIndex("StatusId");
|
||||||
|
|
||||||
b.HasIndex("TenantId");
|
b.HasIndex("TenantId");
|
||||||
|
|
||||||
b.ToTable("ExpensesStatusMapping");
|
b.ToTable("ExpensesStatusMapping");
|
||||||
@ -1482,7 +1489,7 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = new Guid("5cf7f1df-9d1f-4289-add0-1775ad614f25"),
|
Id = new Guid("5cf7f1df-9d1f-4289-add0-1775ad614f25"),
|
||||||
NextStatusId = new Guid("f18c5cfd-7815-4341-8da2-2c2d65778e27"),
|
NextStatusId = new Guid("61578360-3a49-4c34-8604-7b35a3787b95"),
|
||||||
StatusId = new Guid("f18c5cfd-7815-4341-8da2-2c2d65778e27"),
|
StatusId = new Guid("f18c5cfd-7815-4341-8da2-2c2d65778e27"),
|
||||||
TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26")
|
TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26")
|
||||||
},
|
},
|
||||||
@ -1501,6 +1508,13 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26")
|
TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26")
|
||||||
},
|
},
|
||||||
new
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("75bbda6a-6a53-47d1-ad71-5f5f9446a11e"),
|
||||||
|
NextStatusId = new Guid("6537018f-f4e9-4cb3-a210-6c3b2da999d7"),
|
||||||
|
StatusId = new Guid("d1ee5eec-24b6-4364-8673-a8f859c60729"),
|
||||||
|
TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26")
|
||||||
|
},
|
||||||
|
new
|
||||||
{
|
{
|
||||||
Id = new Guid("fddaaf20-4ccc-4f4e-a724-dd310272b356"),
|
Id = new Guid("fddaaf20-4ccc-4f4e-a724-dd310272b356"),
|
||||||
NextStatusId = new Guid("d1ee5eec-24b6-4364-8673-a8f859c60729"),
|
NextStatusId = new Guid("d1ee5eec-24b6-4364-8673-a8f859c60729"),
|
||||||
@ -1535,13 +1549,55 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
b.Property<Guid>("StatusId")
|
b.Property<Guid>("StatusId")
|
||||||
.HasColumnType("char(36)");
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<Guid>("TenantId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("PermissionId");
|
b.HasIndex("PermissionId");
|
||||||
|
|
||||||
b.HasIndex("StatusId");
|
b.HasIndex("StatusId");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId");
|
||||||
|
|
||||||
b.ToTable("StatusPermissionMapping");
|
b.ToTable("StatusPermissionMapping");
|
||||||
|
|
||||||
|
b.HasData(
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("ed893799-1a5f-4311-a077-de93c86ca8fd"),
|
||||||
|
PermissionId = new Guid("1f4bda08-1873-449a-bb66-3e8222bd871b"),
|
||||||
|
StatusId = new Guid("4068007f-c92f-4f37-a907-bc15fe57d4d8"),
|
||||||
|
TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26")
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("4652d73f-fc71-4fe1-9f2f-1e48b342d741"),
|
||||||
|
PermissionId = new Guid("1f4bda08-1873-449a-bb66-3e8222bd871b"),
|
||||||
|
StatusId = new Guid("d1ee5eec-24b6-4364-8673-a8f859c60729"),
|
||||||
|
TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26")
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("cd15f9b9-be45-4deb-9c71-2f23f872dbcd"),
|
||||||
|
PermissionId = new Guid("eaafdd76-8aac-45f9-a530-315589c6deca"),
|
||||||
|
StatusId = new Guid("d1ee5eec-24b6-4364-8673-a8f859c60729"),
|
||||||
|
TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26")
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("f6f26b2f-2fa6-40b7-8601-cbd4bcdda0cc"),
|
||||||
|
PermissionId = new Guid("eaafdd76-8aac-45f9-a530-315589c6deca"),
|
||||||
|
StatusId = new Guid("f18c5cfd-7815-4341-8da2-2c2d65778e27"),
|
||||||
|
TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26")
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("214354e5-daad-4569-ad69-eb5bf4e87fbc"),
|
||||||
|
PermissionId = new Guid("ea5a1529-4ee8-4828-80ea-0e23c9d4dd11"),
|
||||||
|
StatusId = new Guid("61578360-3a49-4c34-8604-7b35a3787b95"),
|
||||||
|
TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26")
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Marco.Pms.Model.Forum.TicketAttachment", b =>
|
modelBuilder.Entity("Marco.Pms.Model.Forum.TicketAttachment", b =>
|
||||||
@ -1840,10 +1896,18 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("char(36)");
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<string>("Color")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
b.Property<string>("Description")
|
b.Property<string>("Description")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("longtext");
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<string>("DisplayName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
b.Property<bool>("IsActive")
|
b.Property<bool>("IsActive")
|
||||||
.HasColumnType("tinyint(1)");
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
@ -1867,7 +1931,9 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = new Guid("297e0d8f-f668-41b5-bfea-e03b354251c8"),
|
Id = new Guid("297e0d8f-f668-41b5-bfea-e03b354251c8"),
|
||||||
|
Color = "#212529",
|
||||||
Description = "Expense has been created but not yet submitted.",
|
Description = "Expense has been created but not yet submitted.",
|
||||||
|
DisplayName = "Draft",
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
IsSystem = true,
|
IsSystem = true,
|
||||||
Name = "Draft",
|
Name = "Draft",
|
||||||
@ -1876,7 +1942,9 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = new Guid("6537018f-f4e9-4cb3-a210-6c3b2da999d7"),
|
Id = new Guid("6537018f-f4e9-4cb3-a210-6c3b2da999d7"),
|
||||||
|
Color = "#0d6efd",
|
||||||
Description = "Reviewer is currently reviewing the expense.",
|
Description = "Reviewer is currently reviewing the expense.",
|
||||||
|
DisplayName = "Review",
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
IsSystem = true,
|
IsSystem = true,
|
||||||
Name = "Review Pending",
|
Name = "Review Pending",
|
||||||
@ -1885,7 +1953,9 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = new Guid("4068007f-c92f-4f37-a907-bc15fe57d4d8"),
|
Id = new Guid("4068007f-c92f-4f37-a907-bc15fe57d4d8"),
|
||||||
|
Color = "#0dcaf0",
|
||||||
Description = "Review is completed, waiting for action of approver.",
|
Description = "Review is completed, waiting for action of approver.",
|
||||||
|
DisplayName = "Approve",
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
IsSystem = true,
|
IsSystem = true,
|
||||||
Name = "Approval Pending",
|
Name = "Approval Pending",
|
||||||
@ -1894,7 +1964,9 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = new Guid("d1ee5eec-24b6-4364-8673-a8f859c60729"),
|
Id = new Guid("d1ee5eec-24b6-4364-8673-a8f859c60729"),
|
||||||
|
Color = "#dc3545",
|
||||||
Description = "Expense was declined, often with a reason(either review rejected or approval rejected.",
|
Description = "Expense was declined, often with a reason(either review rejected or approval rejected.",
|
||||||
|
DisplayName = "Reject",
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
IsSystem = true,
|
IsSystem = true,
|
||||||
Name = "Rejected",
|
Name = "Rejected",
|
||||||
@ -1903,7 +1975,9 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = new Guid("f18c5cfd-7815-4341-8da2-2c2d65778e27"),
|
Id = new Guid("f18c5cfd-7815-4341-8da2-2c2d65778e27"),
|
||||||
|
Color = "#ffc107",
|
||||||
Description = "Approved expense is awaiting final payment.",
|
Description = "Approved expense is awaiting final payment.",
|
||||||
|
DisplayName = "Process",
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
IsSystem = true,
|
IsSystem = true,
|
||||||
Name = "Process Pending",
|
Name = "Process Pending",
|
||||||
@ -1912,7 +1986,9 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = new Guid("61578360-3a49-4c34-8604-7b35a3787b95"),
|
Id = new Guid("61578360-3a49-4c34-8604-7b35a3787b95"),
|
||||||
|
Color = "#198754",
|
||||||
Description = "Expense has been settled.",
|
Description = "Expense has been settled.",
|
||||||
|
DisplayName = "Paid",
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
IsSystem = true,
|
IsSystem = true,
|
||||||
Name = "Processed",
|
Name = "Processed",
|
||||||
@ -3693,6 +3769,12 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("TenantId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
b.HasOne("Marco.Pms.Model.Employees.Employee", "UpdatedBy")
|
b.HasOne("Marco.Pms.Model.Employees.Employee", "UpdatedBy")
|
||||||
.WithMany()
|
.WithMany()
|
||||||
.HasForeignKey("UpdatedById")
|
.HasForeignKey("UpdatedById")
|
||||||
@ -3701,6 +3783,8 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
|
|
||||||
b.Navigation("Expense");
|
b.Navigation("Expense");
|
||||||
|
|
||||||
|
b.Navigation("Tenant");
|
||||||
|
|
||||||
b.Navigation("UpdatedBy");
|
b.Navigation("UpdatedBy");
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -3796,23 +3880,33 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("TenantId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
b.Navigation("Expenses");
|
b.Navigation("Expenses");
|
||||||
|
|
||||||
b.Navigation("ExpensesReimburse");
|
b.Navigation("ExpensesReimburse");
|
||||||
|
|
||||||
|
b.Navigation("Tenant");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Marco.Pms.Model.Expenses.ExpensesStatusMapping", b =>
|
modelBuilder.Entity("Marco.Pms.Model.Expenses.ExpensesStatusMapping", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Marco.Pms.Model.Master.ExpensesStatusMaster", "Status")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("ExpeStatusIdnsesId");
|
|
||||||
|
|
||||||
b.HasOne("Marco.Pms.Model.Master.ExpensesStatusMaster", "NextStatus")
|
b.HasOne("Marco.Pms.Model.Master.ExpensesStatusMaster", "NextStatus")
|
||||||
.WithMany()
|
.WithMany()
|
||||||
.HasForeignKey("NextStatusId")
|
.HasForeignKey("NextStatusId")
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Marco.Pms.Model.Master.ExpensesStatusMaster", "Status")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("StatusId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant")
|
b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant")
|
||||||
.WithMany()
|
.WithMany()
|
||||||
.HasForeignKey("TenantId")
|
.HasForeignKey("TenantId")
|
||||||
@ -3840,9 +3934,17 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("TenantId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
b.Navigation("Permission");
|
b.Navigation("Permission");
|
||||||
|
|
||||||
b.Navigation("Status");
|
b.Navigation("Status");
|
||||||
|
|
||||||
|
b.Navigation("Tenant");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Marco.Pms.Model.Forum.TicketAttachment", b =>
|
modelBuilder.Entity("Marco.Pms.Model.Forum.TicketAttachment", b =>
|
||||||
|
@ -4,6 +4,6 @@
|
|||||||
{
|
{
|
||||||
public Guid ExpenseId { get; set; }
|
public Guid ExpenseId { get; set; }
|
||||||
public Guid StatusId { get; set; }
|
public Guid StatusId { get; set; }
|
||||||
public string? Description { get; set; }
|
public string? Comment { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
using Marco.Pms.Model.Employees;
|
using Marco.Pms.Model.Employees;
|
||||||
|
using Marco.Pms.Model.Utilities;
|
||||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
|
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
namespace Marco.Pms.Model.Expenses
|
namespace Marco.Pms.Model.Expenses
|
||||||
{
|
{
|
||||||
public class ExpenseLog
|
public class ExpenseLog : TenantRelation
|
||||||
{
|
{
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
public Guid ExpenseId { get; set; }
|
public Guid ExpenseId { get; set; }
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
|
using Marco.Pms.Model.Utilities;
|
||||||
|
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
namespace Marco.Pms.Model.Expenses
|
namespace Marco.Pms.Model.Expenses
|
||||||
{
|
{
|
||||||
public class ExpensesReimburseMapping
|
public class ExpensesReimburseMapping : TenantRelation
|
||||||
{
|
{
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
public Guid ExpensesId { get; set; }
|
public Guid ExpensesId { get; set; }
|
||||||
|
@ -11,7 +11,7 @@ namespace Marco.Pms.Model.Expenses
|
|||||||
public Guid StatusId { get; set; }
|
public Guid StatusId { get; set; }
|
||||||
|
|
||||||
[ValidateNever]
|
[ValidateNever]
|
||||||
[ForeignKey("ExpeStatusIdnsesId")]
|
[ForeignKey("StatusId")]
|
||||||
public ExpensesStatusMaster? Status { get; set; }
|
public ExpensesStatusMaster? Status { get; set; }
|
||||||
public Guid NextStatusId { get; set; }
|
public Guid NextStatusId { get; set; }
|
||||||
|
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
using Marco.Pms.Model.Entitlements;
|
using Marco.Pms.Model.Entitlements;
|
||||||
using Marco.Pms.Model.Master;
|
using Marco.Pms.Model.Master;
|
||||||
|
using Marco.Pms.Model.Utilities;
|
||||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
|
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
namespace Marco.Pms.Model.Expenses
|
namespace Marco.Pms.Model.Expenses
|
||||||
{
|
{
|
||||||
public class StatusPermissionMapping
|
public class StatusPermissionMapping : TenantRelation
|
||||||
{
|
{
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
public Guid StatusId { get; set; }
|
public Guid StatusId { get; set; }
|
||||||
|
@ -6,7 +6,9 @@ namespace Marco.Pms.Model.Master
|
|||||||
{
|
{
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
public string Name { get; set; } = string.Empty;
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public string DisplayName { get; set; } = string.Empty;
|
||||||
public string Description { get; set; } = string.Empty;
|
public string Description { get; set; } = string.Empty;
|
||||||
|
public string Color { get; set; } = string.Empty;
|
||||||
public bool IsSystem { get; set; } = false;
|
public bool IsSystem { get; set; } = false;
|
||||||
public bool IsActive { get; set; } = true;
|
public bool IsActive { get; set; } = true;
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,9 @@
|
|||||||
{
|
{
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
public string Name { get; set; } = string.Empty;
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public string DisplayName { get; set; } = string.Empty;
|
||||||
public string Description { get; set; } = string.Empty;
|
public string Description { get; set; } = string.Empty;
|
||||||
|
public string? Color { get; set; }
|
||||||
public bool IsSystem { get; set; } = false;
|
public bool IsSystem { get; set; } = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,20 +1,9 @@
|
|||||||
using AutoMapper;
|
using Marco.Pms.Model.Dtos.Expenses;
|
||||||
using Marco.Pms.DataAccess.Data;
|
|
||||||
using Marco.Pms.Model.Dtos.Expenses;
|
|
||||||
using Marco.Pms.Model.Entitlements;
|
|
||||||
using Marco.Pms.Model.Expenses;
|
|
||||||
using Marco.Pms.Model.Utilities;
|
using Marco.Pms.Model.Utilities;
|
||||||
using Marco.Pms.Model.ViewModels.Expanses;
|
using Marco.Pms.Services.Service.ServiceInterfaces;
|
||||||
using Marco.Pms.Model.ViewModels.Master;
|
|
||||||
using Marco.Pms.Model.ViewModels.Projects;
|
|
||||||
using Marco.Pms.Services.Service;
|
|
||||||
using MarcoBMS.Services.Helpers;
|
using MarcoBMS.Services.Helpers;
|
||||||
using MarcoBMS.Services.Service;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using System.Text.Json;
|
|
||||||
using Document = Marco.Pms.Model.DocumentManager.Document;
|
|
||||||
|
|
||||||
|
|
||||||
namespace Marco.Pms.Services.Controllers
|
namespace Marco.Pms.Services.Controllers
|
||||||
@ -24,31 +13,19 @@ namespace Marco.Pms.Services.Controllers
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public class ExpenseController : ControllerBase
|
public class ExpenseController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly IDbContextFactory<ApplicationDbContext> _dbContextFactory;
|
|
||||||
private readonly ApplicationDbContext _context;
|
|
||||||
private readonly UserHelper _userHelper;
|
private readonly UserHelper _userHelper;
|
||||||
private readonly ILoggingService _logger;
|
private readonly IExpensesService _expensesService;
|
||||||
private readonly S3UploadService _s3Service;
|
private readonly ISignalRService _signalR;
|
||||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
|
||||||
private readonly IMapper _mapper;
|
|
||||||
private readonly Guid tenantId;
|
private readonly Guid tenantId;
|
||||||
private static readonly Guid Draft = Guid.Parse("297e0d8f-f668-41b5-bfea-e03b354251c8");
|
|
||||||
public ExpenseController(
|
public ExpenseController(
|
||||||
IDbContextFactory<ApplicationDbContext> dbContextFactory,
|
|
||||||
ApplicationDbContext context,
|
|
||||||
UserHelper userHelper,
|
UserHelper userHelper,
|
||||||
IServiceScopeFactory serviceScopeFactory,
|
IExpensesService expensesService,
|
||||||
ILoggingService logger,
|
ISignalRService signalR
|
||||||
S3UploadService s3Service,
|
)
|
||||||
IMapper mapper)
|
|
||||||
{
|
{
|
||||||
_dbContextFactory = dbContextFactory;
|
|
||||||
_context = context;
|
|
||||||
_userHelper = userHelper;
|
_userHelper = userHelper;
|
||||||
_logger = logger;
|
_expensesService = expensesService;
|
||||||
_serviceScopeFactory = serviceScopeFactory;
|
_signalR = signalR;
|
||||||
_s3Service = s3Service;
|
|
||||||
_mapper = mapper;
|
|
||||||
tenantId = userHelper.GetTenantId();
|
tenantId = userHelper.GetTenantId();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,182 +41,9 @@ namespace Marco.Pms.Services.Controllers
|
|||||||
[HttpGet("list")]
|
[HttpGet("list")]
|
||||||
public async Task<IActionResult> GetExpensesList(string? filter, int pageSize = 20, int pageNumber = 1)
|
public async Task<IActionResult> GetExpensesList(string? filter, int pageSize = 20, int pageNumber = 1)
|
||||||
{
|
{
|
||||||
try
|
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||||
{
|
var response = await _expensesService.GetExpensesListAsync(loggedInEmployee, tenantId, filter, pageSize, pageNumber);
|
||||||
_logger.LogInfo(
|
return StatusCode(response.StatusCode, response);
|
||||||
"Attempting to fetch expenses list for PageNumber: {PageNumber}, PageSize: {PageSize} with Filter: {Filter}",
|
|
||||||
pageNumber, pageSize, filter ?? "");
|
|
||||||
|
|
||||||
// 1. --- Get User and Permissions ---
|
|
||||||
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
|
||||||
if (loggedInEmployee == null)
|
|
||||||
{
|
|
||||||
// This is an authentication/authorization issue. The user should be logged in.
|
|
||||||
_logger.LogWarning("Could not find an employee for the current logged-in user.");
|
|
||||||
return Unauthorized(ApiResponse<object>.ErrorResponse("User not found or not authenticated.", 401));
|
|
||||||
}
|
|
||||||
Guid loggedInEmployeeId = loggedInEmployee.Id;
|
|
||||||
|
|
||||||
var hasViewSelfPermissionTask = Task.Run(async () =>
|
|
||||||
{
|
|
||||||
using var scope = _serviceScopeFactory.CreateScope();
|
|
||||||
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
|
|
||||||
return await permissionService.HasPermission(PermissionsMaster.ExpenseViewSelf, loggedInEmployeeId);
|
|
||||||
});
|
|
||||||
|
|
||||||
var hasViewAllPermissionTask = Task.Run(async () =>
|
|
||||||
{
|
|
||||||
using var scope = _serviceScopeFactory.CreateScope();
|
|
||||||
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
|
|
||||||
return await permissionService.HasPermission(PermissionsMaster.ExpenseViewAll, loggedInEmployeeId);
|
|
||||||
});
|
|
||||||
|
|
||||||
await Task.WhenAll(hasViewSelfPermissionTask, hasViewAllPermissionTask);
|
|
||||||
|
|
||||||
// 2. --- Build Base Query and Apply Permissions ---
|
|
||||||
// Start with a base IQueryable. Filters will be chained onto this.
|
|
||||||
var expensesQuery = _context.Expenses
|
|
||||||
.Include(e => e.ExpensesType)
|
|
||||||
.Include(e => e.Project)
|
|
||||||
.Include(e => e.PaidBy)
|
|
||||||
.ThenInclude(e => e!.JobRole)
|
|
||||||
.Include(e => e.PaymentMode)
|
|
||||||
.Include(e => e.Status)
|
|
||||||
.Include(e => e.CreatedBy)
|
|
||||||
.Where(e => e.TenantId == tenantId); // Always filter by TenantId first.
|
|
||||||
|
|
||||||
// Apply permission-based filtering BEFORE any other filters or pagination.
|
|
||||||
if (hasViewAllPermissionTask.Result)
|
|
||||||
{
|
|
||||||
// User has 'View All' permission, no initial restriction on who created the expense.
|
|
||||||
_logger.LogInfo("User {EmployeeId} has 'View All' permission.", loggedInEmployeeId);
|
|
||||||
}
|
|
||||||
else if (hasViewSelfPermissionTask.Result)
|
|
||||||
{
|
|
||||||
// User only has 'View Self' permission, so restrict the query to their own expenses.
|
|
||||||
_logger.LogInfo("User {EmployeeId} has 'View Self' permission. Restricting query to their expenses.", loggedInEmployeeId);
|
|
||||||
expensesQuery = expensesQuery.Where(e => e.CreatedById == loggedInEmployeeId);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// User has neither required permission. Deny access.
|
|
||||||
_logger.LogWarning("Access DENIED for employee {EmployeeId} attempting to get expenses list.", loggedInEmployeeId);
|
|
||||||
return Ok(ApiResponse<object>.SuccessResponse(new List<ExpenseList>(), "You do not have permission to view any expenses.", 200));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. --- Deserialize Filter and Apply ---
|
|
||||||
ExpensesFilter? expenseFilter = TryDeserializeFilter(filter);
|
|
||||||
|
|
||||||
if (expenseFilter != null)
|
|
||||||
{
|
|
||||||
// CRITICAL FIX: Apply filters cumulatively using multiple `if` statements, not `if-else if`.
|
|
||||||
if (expenseFilter.StartDate.HasValue && expenseFilter.EndDate.HasValue)
|
|
||||||
{
|
|
||||||
expensesQuery = expensesQuery.Where(e => e.CreatedAt.Date >= expenseFilter.StartDate.Value.Date && e.CreatedAt.Date <= expenseFilter.EndDate.Value.Date);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (expenseFilter.ProjectIds?.Any() == true)
|
|
||||||
{
|
|
||||||
expensesQuery = expensesQuery.Where(e => expenseFilter.ProjectIds.Contains(e.ProjectId));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (expenseFilter.StatusIds?.Any() == true)
|
|
||||||
{
|
|
||||||
expensesQuery = expensesQuery.Where(e => expenseFilter.StatusIds.Contains(e.StatusId));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (expenseFilter.PaidById?.Any() == true)
|
|
||||||
{
|
|
||||||
expensesQuery = expensesQuery.Where(e => expenseFilter.PaidById.Contains(e.PaidById));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only allow filtering by 'CreatedBy' if the user has 'View All' permission.
|
|
||||||
if (expenseFilter.CreatedByIds?.Any() == true && hasViewAllPermissionTask.Result)
|
|
||||||
{
|
|
||||||
expensesQuery = expensesQuery.Where(e => expenseFilter.CreatedByIds.Contains(e.CreatedById));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. --- Apply Ordering and Pagination ---
|
|
||||||
// This should be the last step before executing the query.
|
|
||||||
var paginatedQuery = expensesQuery
|
|
||||||
.OrderByDescending(e => e.CreatedAt)
|
|
||||||
.Skip((pageNumber - 1) * pageSize)
|
|
||||||
.Take(pageSize);
|
|
||||||
|
|
||||||
// 5. --- Execute Query and Map Results ---
|
|
||||||
var expensesList = await paginatedQuery.ToListAsync();
|
|
||||||
|
|
||||||
if (!expensesList.Any())
|
|
||||||
{
|
|
||||||
_logger.LogInfo("No expenses found matching the criteria for employee {EmployeeId}.", loggedInEmployeeId);
|
|
||||||
return Ok(ApiResponse<object>.SuccessResponse(new List<ExpenseList>(), "No expenses found for the given criteria.", 200));
|
|
||||||
}
|
|
||||||
|
|
||||||
var response = _mapper.Map<List<ExpenseList>>(expensesList);
|
|
||||||
|
|
||||||
// 6. --- Efficiently Fetch and Append 'Next Status' Information ---
|
|
||||||
var statusIds = expensesList.Select(e => e.StatusId).Distinct().ToList();
|
|
||||||
|
|
||||||
var statusMappings = await _context.ExpensesStatusMapping
|
|
||||||
.Include(sm => sm.NextStatus)
|
|
||||||
.Where(sm => statusIds.Contains(sm.StatusId))
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
// Use a Lookup for efficient O(1) mapping. This is much better than repeated `.Where()` in a loop.
|
|
||||||
var statusMapLookup = statusMappings.ToLookup(sm => sm.StatusId);
|
|
||||||
|
|
||||||
foreach (var expense in response)
|
|
||||||
{
|
|
||||||
if (expense.Status?.Id != null && statusMapLookup.Contains(expense.Status.Id))
|
|
||||||
{
|
|
||||||
expense.NextStatus = statusMapLookup[expense.Status.Id]
|
|
||||||
.Select(sm => _mapper.Map<ExpensesStatusMasterVM>(sm.NextStatus))
|
|
||||||
.ToList();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
expense.NextStatus = new List<ExpensesStatusMasterVM>(); // Ensure it's never null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 7. --- Return Final Success Response ---
|
|
||||||
var message = $"{response.Count} expense records fetched successfully.";
|
|
||||||
_logger.LogInfo(message);
|
|
||||||
return StatusCode(200, ApiResponse<object>.SuccessResponse(response, message, 200));
|
|
||||||
}
|
|
||||||
catch (DbUpdateException dbEx)
|
|
||||||
{
|
|
||||||
_logger.LogError(dbEx, "Databsae Exception occured while fetching list expenses");
|
|
||||||
return BadRequest(ApiResponse<object>.ErrorResponse("Databsae Exception", new
|
|
||||||
{
|
|
||||||
Message = dbEx.Message,
|
|
||||||
StackTrace = dbEx.StackTrace,
|
|
||||||
Source = dbEx.Source,
|
|
||||||
innerexcption = new
|
|
||||||
{
|
|
||||||
Message = dbEx.InnerException?.Message,
|
|
||||||
StackTrace = dbEx.InnerException?.StackTrace,
|
|
||||||
Source = dbEx.InnerException?.Source,
|
|
||||||
}
|
|
||||||
}, 400));
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error occured while fetching list expenses");
|
|
||||||
return BadRequest(ApiResponse<object>.ErrorResponse("Error Occured", new
|
|
||||||
{
|
|
||||||
Message = ex.Message,
|
|
||||||
StackTrace = ex.StackTrace,
|
|
||||||
Source = ex.Source,
|
|
||||||
innerexcption = new
|
|
||||||
{
|
|
||||||
Message = ex.InnerException?.Message,
|
|
||||||
StackTrace = ex.InnerException?.StackTrace,
|
|
||||||
Source = ex.InnerException?.Source,
|
|
||||||
}
|
|
||||||
}, 400));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("details/{id}")]
|
[HttpGet("details/{id}")]
|
||||||
@ -257,355 +61,38 @@ namespace Marco.Pms.Services.Controllers
|
|||||||
/// <returns>An IActionResult indicating the result of the creation operation.</returns>
|
/// <returns>An IActionResult indicating the result of the creation operation.</returns>
|
||||||
|
|
||||||
[HttpPost("create")]
|
[HttpPost("create")]
|
||||||
public async Task<IActionResult> CreateExpense([FromBody] CreateExpensesDto dto)
|
public async Task<IActionResult> CreateExpense([FromBody] CreateExpensesDto model)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Starting CreateExpense for Project {ProjectId}", dto.ProjectId);
|
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||||
// The entire operation is wrapped in a transaction to ensure data consistency.
|
var response = await _expensesService.CreateExpenseAsync(model, loggedInEmployee, tenantId);
|
||||||
await using var transaction = await _context.Database.BeginTransactionAsync();
|
return StatusCode(response.StatusCode, response);
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
|
||||||
|
|
||||||
// 1. Authorization & Validation: Run all I/O-bound checks concurrently using factories for safety.
|
|
||||||
|
|
||||||
// PERMISSION CHECKS: Use IServiceScopeFactory for thread-safe access to scoped services.
|
|
||||||
var hasUploadPermissionTask = Task.Run(async () => // Task.Run is acceptable here to create a new scope, but let's do it cleaner.
|
|
||||||
{
|
|
||||||
using var scope = _serviceScopeFactory.CreateScope();
|
|
||||||
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
|
|
||||||
return await permissionService.HasPermission(PermissionsMaster.ExpenseUpload, loggedInEmployee.Id);
|
|
||||||
});
|
|
||||||
|
|
||||||
var hasProjectPermissionTask = Task.Run(async () =>
|
|
||||||
{
|
|
||||||
using var scope = _serviceScopeFactory.CreateScope();
|
|
||||||
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
|
|
||||||
return await permissionService.HasProjectPermission(loggedInEmployee, dto.ProjectId);
|
|
||||||
});
|
|
||||||
|
|
||||||
// VALIDATION CHECKS: Use IDbContextFactory for thread-safe, parallel database queries.
|
|
||||||
// Each task gets its own DbContext instance.
|
|
||||||
var projectTask = Task.Run(async () =>
|
|
||||||
{
|
|
||||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
|
||||||
return await dbContext.Projects.AsNoTracking().FirstOrDefaultAsync(p => p.Id == dto.ProjectId);
|
|
||||||
});
|
|
||||||
var expenseTypeTask = Task.Run(async () =>
|
|
||||||
{
|
|
||||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
|
||||||
return await dbContext.ExpensesTypeMaster.AsNoTracking().FirstOrDefaultAsync(et => et.Id == dto.ExpensesTypeId);
|
|
||||||
});
|
|
||||||
var paymentModeTask = Task.Run(async () =>
|
|
||||||
{
|
|
||||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
|
||||||
return await dbContext.PaymentModeMatser.AsNoTracking().FirstOrDefaultAsync(pm => pm.Id == dto.PaymentModeId);
|
|
||||||
});
|
|
||||||
var statusTask = Task.Run(async () =>
|
|
||||||
{
|
|
||||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
|
||||||
return await dbContext.ExpensesStatusMaster.AsNoTracking().FirstOrDefaultAsync(es => es.Id == Draft);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// Await all prerequisite checks at once.
|
|
||||||
await Task.WhenAll(
|
|
||||||
hasUploadPermissionTask, hasProjectPermissionTask,
|
|
||||||
projectTask, expenseTypeTask, paymentModeTask, statusTask
|
|
||||||
);
|
|
||||||
|
|
||||||
// Await all prerequisite checks at once.
|
|
||||||
await Task.WhenAll(
|
|
||||||
hasUploadPermissionTask, hasProjectPermissionTask,
|
|
||||||
projectTask, expenseTypeTask, paymentModeTask, statusTask
|
|
||||||
);
|
|
||||||
|
|
||||||
// 2. Aggregate and Check Results
|
|
||||||
if (!await hasUploadPermissionTask || !await hasProjectPermissionTask)
|
|
||||||
{
|
|
||||||
_logger.LogWarning("Access DENIED for employee {EmployeeId} on project {ProjectId}.", loggedInEmployee.Id, dto.ProjectId);
|
|
||||||
return StatusCode(403, ApiResponse<object>.ErrorResponse("Access Denied.", "You do not have permission to upload expenses for this project.", 403));
|
|
||||||
}
|
|
||||||
|
|
||||||
var validationErrors = new List<string>();
|
|
||||||
var project = await projectTask;
|
|
||||||
var expenseType = await expenseTypeTask;
|
|
||||||
var paymentMode = await paymentModeTask;
|
|
||||||
var status = await statusTask;
|
|
||||||
|
|
||||||
if (project == null) validationErrors.Add("Project not found.");
|
|
||||||
if (expenseType == null) validationErrors.Add("Expense Type not found.");
|
|
||||||
if (paymentMode == null) validationErrors.Add("Payment Mode not found.");
|
|
||||||
if (status == null) validationErrors.Add("Default status 'Draft' not found.");
|
|
||||||
|
|
||||||
if (validationErrors.Any())
|
|
||||||
{
|
|
||||||
await transaction.RollbackAsync();
|
|
||||||
var errorMessage = string.Join(" ", validationErrors);
|
|
||||||
_logger.LogWarning("Expense creation failed due to validation errors: {ValidationErrors}", errorMessage);
|
|
||||||
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid input data.", errorMessage, 400));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Entity Creation
|
|
||||||
var expense = _mapper.Map<Expenses>(dto);
|
|
||||||
expense.CreatedById = loggedInEmployee.Id;
|
|
||||||
expense.CreatedAt = DateTime.UtcNow;
|
|
||||||
expense.TenantId = tenantId;
|
|
||||||
expense.IsActive = true;
|
|
||||||
expense.StatusId = Draft;
|
|
||||||
|
|
||||||
_context.Expenses.Add(expense);
|
|
||||||
|
|
||||||
// 4. Process Attachments
|
|
||||||
if (dto.BillAttachments?.Any() ?? false)
|
|
||||||
{
|
|
||||||
await ProcessAndUploadAttachmentsAsync(dto.BillAttachments, expense, loggedInEmployee.Id, tenantId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Database Commit
|
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
|
|
||||||
// 6. Transaction Commit
|
|
||||||
await transaction.CommitAsync();
|
|
||||||
|
|
||||||
var response = _mapper.Map<ExpenseList>(expense);
|
|
||||||
response.Project = _mapper.Map<ProjectInfoVM>(project);
|
|
||||||
response.Status = _mapper.Map<ExpensesStatusMasterVM>(status);
|
|
||||||
response.PaymentMode = _mapper.Map<PaymentModeMatserVM>(paymentMode);
|
|
||||||
response.ExpensesType = _mapper.Map<ExpensesTypeMasterVM>(expenseType);
|
|
||||||
|
|
||||||
_logger.LogInfo("Successfully created Expense {ExpenseId} for Project {ProjectId}.", expense.Id, expense.ProjectId);
|
|
||||||
return StatusCode(201, ApiResponse<object>.SuccessResponse(response, "Expense created successfully.", 201));
|
|
||||||
}
|
|
||||||
catch (ArgumentException ex) // Catches bad Base64 from attachment pre-validation
|
|
||||||
{
|
|
||||||
await transaction.RollbackAsync();
|
|
||||||
_logger.LogError(ex, "Invalid argument during expense creation for project {ProjectId}.", dto.ProjectId);
|
|
||||||
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Request Data.", new
|
|
||||||
{
|
|
||||||
Message = ex.Message,
|
|
||||||
StackTrace = ex.StackTrace,
|
|
||||||
Source = ex.Source,
|
|
||||||
innerexcption = new
|
|
||||||
{
|
|
||||||
Message = ex.InnerException?.Message,
|
|
||||||
StackTrace = ex.InnerException?.StackTrace,
|
|
||||||
Source = ex.InnerException?.Source,
|
|
||||||
}
|
|
||||||
}, 400));
|
|
||||||
}
|
|
||||||
catch (Exception ex) // General-purpose catch for unexpected errors (e.g., S3 or DB connection failure)
|
|
||||||
{
|
|
||||||
await transaction.RollbackAsync();
|
|
||||||
_logger.LogError(ex, "An unhandled exception occurred while creating an expense for project {ProjectId}.", dto.ProjectId);
|
|
||||||
return StatusCode(500, ApiResponse<object>.ErrorResponse("An internal server error occurred.", new
|
|
||||||
{
|
|
||||||
Message = ex.Message,
|
|
||||||
StackTrace = ex.StackTrace,
|
|
||||||
Source = ex.Source,
|
|
||||||
innerexcption = new
|
|
||||||
{
|
|
||||||
Message = ex.InnerException?.Message,
|
|
||||||
StackTrace = ex.InnerException?.StackTrace,
|
|
||||||
Source = ex.InnerException?.Source,
|
|
||||||
}
|
|
||||||
}, 500));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("action")]
|
[HttpPost("action")]
|
||||||
public async Task<IActionResult> ChangeStatus([FromBody] ExpenseRecordDto model)
|
public async Task<IActionResult> ChangeStatus([FromBody] ExpenseRecordDto model)
|
||||||
{
|
{
|
||||||
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||||
var exsitingExpenses = await _context.Expenses
|
var response = await _expensesService.ChangeStatusAsync(model, loggedInEmployee, tenantId);
|
||||||
.FirstOrDefaultAsync(e => e.Id == model.ExpenseId && e.TenantId == tenantId);
|
if (response.Success)
|
||||||
|
|
||||||
if (exsitingExpenses == null)
|
|
||||||
{
|
{
|
||||||
return NotFound(ApiResponse<object>.ErrorResponse("Expense not found", "Expense not found", 404));
|
var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Expanse", Response = response.Data };
|
||||||
|
await _signalR.SendNotificationAsync(notification);
|
||||||
}
|
}
|
||||||
|
return StatusCode(response.StatusCode, response);
|
||||||
exsitingExpenses.StatusId = model.StatusId;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
catch (DbUpdateConcurrencyException dbEx)
|
|
||||||
{
|
|
||||||
// --- Step 3: Handle Concurrency Conflicts ---
|
|
||||||
// This happens if another user modified the project after we fetched it.
|
|
||||||
_logger.LogError(dbEx, "Error occured while update status of expanse.");
|
|
||||||
return StatusCode(500, ApiResponse<object>.ErrorResponse("Error occured while update status of expanse.", new
|
|
||||||
{
|
|
||||||
Message = dbEx.Message,
|
|
||||||
StackTrace = dbEx.StackTrace,
|
|
||||||
Source = dbEx.Source,
|
|
||||||
innerexcption = new
|
|
||||||
{
|
|
||||||
Message = dbEx.InnerException?.Message,
|
|
||||||
StackTrace = dbEx.InnerException?.StackTrace,
|
|
||||||
Source = dbEx.InnerException?.Source,
|
|
||||||
}
|
|
||||||
}, 500));
|
|
||||||
}
|
|
||||||
var response = _mapper.Map<ExpenseList>(exsitingExpenses);
|
|
||||||
return Ok(ApiResponse<object>.SuccessResponse(response));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut("edit/{id}")]
|
[HttpPut("edit/{id}")]
|
||||||
public async Task<IActionResult> UpdateExpanse(Guid id, [FromBody] UpdateExpensesDto model)
|
public async Task<IActionResult> UpdateExpanse(Guid id, [FromBody] UpdateExpensesDto model)
|
||||||
{
|
{
|
||||||
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||||
var exsitingExpense = await _context.Expenses.FirstOrDefaultAsync(e => e.Id == model.Id && e.TenantId == tenantId);
|
var response = await _expensesService.UpdateExpanseAsync(id, model, loggedInEmployee, tenantId);
|
||||||
|
return StatusCode(response.StatusCode, response);
|
||||||
|
|
||||||
if (exsitingExpense == null)
|
|
||||||
{
|
|
||||||
return NotFound(ApiResponse<object>.ErrorResponse("Expense not found", "Expense not found", 404));
|
|
||||||
}
|
|
||||||
_mapper.Map(model, exsitingExpense);
|
|
||||||
_context.Entry(exsitingExpense).State = EntityState.Modified;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
_logger.LogInfo("Successfully updated project {ProjectId} by user {UserId}.", id, loggedInEmployee.Id);
|
|
||||||
}
|
|
||||||
catch (DbUpdateConcurrencyException ex)
|
|
||||||
{
|
|
||||||
// --- Step 3: Handle Concurrency Conflicts ---
|
|
||||||
// This happens if another user modified the project after we fetched it.
|
|
||||||
_logger.LogError(ex, "Concurrency conflict while updating project {ProjectId} ", id);
|
|
||||||
return StatusCode(409, ApiResponse<object>.ErrorResponse("Conflict occurred.", "This project has been modified by someone else. Please refresh and try again.", 409));
|
|
||||||
}
|
|
||||||
var response = _mapper.Map<ExpenseList>(exsitingExpense);
|
|
||||||
return Ok(ApiResponse<object>.SuccessResponse(response));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("delete/{id}")]
|
[HttpDelete("delete/{id}")]
|
||||||
public void Delete(int id)
|
public void Delete(int id)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
#region =================================================================== Helper Functions ===================================================================
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Deserializes the filter string, handling multiple potential formats (e.g., direct JSON vs. escaped JSON string).
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="filter">The JSON filter string from the request.</param>
|
|
||||||
/// <returns>An <see cref="ExpensesFilter"/> object or null if deserialization fails.</returns>
|
|
||||||
private ExpensesFilter? TryDeserializeFilter(string? filter)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(filter))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
|
|
||||||
ExpensesFilter? expenseFilter = null;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// First, try to deserialize directly. This is the expected case (e.g., from a web client).
|
|
||||||
expenseFilter = JsonSerializer.Deserialize<ExpensesFilter>(filter, options);
|
|
||||||
}
|
|
||||||
catch (JsonException ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "[{MethodName}] Failed to directly deserialize filter. Attempting to unescape and re-parse. Filter: {Filter}", nameof(TryDeserializeFilter), filter);
|
|
||||||
|
|
||||||
// If direct deserialization fails, it might be an escaped string (common with tools like Postman or some mobile clients).
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Unescape the string first, then deserialize the result.
|
|
||||||
string unescapedJsonString = JsonSerializer.Deserialize<string>(filter, options) ?? "";
|
|
||||||
if (!string.IsNullOrWhiteSpace(unescapedJsonString))
|
|
||||||
{
|
|
||||||
expenseFilter = JsonSerializer.Deserialize<ExpensesFilter>(unescapedJsonString, options);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (JsonException ex1)
|
|
||||||
{
|
|
||||||
// If both attempts fail, log the final error and return null.
|
|
||||||
_logger.LogError(ex1, "[{MethodName}] All attempts to deserialize the filter failed. Filter will be ignored. Filter: {Filter}", nameof(TryDeserializeFilter), filter);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return expenseFilter;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Processes and uploads attachments concurrently, then adds the resulting entities to the main DbContext.
|
|
||||||
/// </summary>
|
|
||||||
private async Task ProcessAndUploadAttachmentsAsync(IEnumerable<FileUploadModel> attachments, Expenses expense, Guid employeeId, Guid tenantId)
|
|
||||||
{
|
|
||||||
// Pre-validate all attachments to fail fast before any uploads.
|
|
||||||
foreach (var attachment in attachments)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(attachment.Base64Data) || !_s3Service.IsBase64String(attachment.Base64Data))
|
|
||||||
{
|
|
||||||
throw new ArgumentException($"Invalid or missing Base64 data for attachment: {attachment.FileName ?? "N/A"}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var batchId = Guid.NewGuid();
|
|
||||||
|
|
||||||
// Create a list of tasks to be executed concurrently.
|
|
||||||
var processingTasks = attachments.Select(attachment =>
|
|
||||||
ProcessSingleAttachmentAsync(attachment, expense, employeeId, tenantId, batchId)
|
|
||||||
).ToList();
|
|
||||||
|
|
||||||
var results = await Task.WhenAll(processingTasks);
|
|
||||||
|
|
||||||
// This part is thread-safe as it runs after all concurrent tasks are complete.
|
|
||||||
foreach (var (document, billAttachment) in results)
|
|
||||||
{
|
|
||||||
_context.Documents.Add(document);
|
|
||||||
_context.BillAttachments.Add(billAttachment);
|
|
||||||
}
|
|
||||||
_logger.LogInfo("{AttachmentCount} attachments processed and staged for saving.", results.Length);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Handles the logic for a single attachment: upload to S3 and create corresponding entities.
|
|
||||||
/// </summary>
|
|
||||||
private async Task<(Document document, BillAttachments billAttachment)> ProcessSingleAttachmentAsync(
|
|
||||||
FileUploadModel attachment, Expenses expense, Guid employeeId, Guid tenantId, Guid batchId)
|
|
||||||
{
|
|
||||||
var base64Data = attachment.Base64Data!.Contains(',') ? attachment.Base64Data[(attachment.Base64Data.IndexOf(",") + 1)..] : attachment.Base64Data;
|
|
||||||
var fileType = _s3Service.GetContentTypeFromBase64(base64Data);
|
|
||||||
var fileName = _s3Service.GenerateFileName(fileType, expense.Id, "Expense");
|
|
||||||
var objectKey = $"tenant-{tenantId}/project-{expense.ProjectId}/Expenses/{fileName}";
|
|
||||||
|
|
||||||
// Await the I/O-bound upload operation directly.
|
|
||||||
await _s3Service.UploadFileAsync(base64Data, fileType, objectKey);
|
|
||||||
_logger.LogInfo("Uploaded file to S3 with key: {ObjectKey}", objectKey);
|
|
||||||
|
|
||||||
return CreateAttachmentEntities(batchId, expense.Id, employeeId, tenantId, objectKey, attachment);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A private static helper method to create Document and BillAttachment entities.
|
|
||||||
/// This remains unchanged as it's a pure data-shaping method.
|
|
||||||
/// </summary>
|
|
||||||
private static (Document document, BillAttachments billAttachment) CreateAttachmentEntities(
|
|
||||||
Guid batchId, Guid expenseId, Guid uploadedById, Guid tenantId, string s3Key, FileUploadModel attachmentDto)
|
|
||||||
{
|
|
||||||
var document = new Document
|
|
||||||
{
|
|
||||||
BatchId = batchId,
|
|
||||||
UploadedById = uploadedById,
|
|
||||||
FileName = attachmentDto.FileName ?? "",
|
|
||||||
ContentType = attachmentDto.ContentType ?? "",
|
|
||||||
S3Key = s3Key,
|
|
||||||
FileSize = attachmentDto.FileSize,
|
|
||||||
UploadedAt = DateTime.UtcNow,
|
|
||||||
TenantId = tenantId
|
|
||||||
};
|
|
||||||
var billAttachment = new BillAttachments { Document = document, ExpensesId = expenseId, TenantId = tenantId };
|
|
||||||
return (document, billAttachment);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -172,6 +172,7 @@ builder.Services.AddScoped<RefreshTokenService>();
|
|||||||
builder.Services.AddScoped<PermissionServices>();
|
builder.Services.AddScoped<PermissionServices>();
|
||||||
builder.Services.AddScoped<ISignalRService, SignalRService>();
|
builder.Services.AddScoped<ISignalRService, SignalRService>();
|
||||||
builder.Services.AddScoped<IProjectServices, ProjectServices>();
|
builder.Services.AddScoped<IProjectServices, ProjectServices>();
|
||||||
|
builder.Services.AddScoped<IExpensesService, ExpensesService>();
|
||||||
builder.Services.AddScoped<IMasterService, MasterService>();
|
builder.Services.AddScoped<IMasterService, MasterService>();
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
895
Marco.Pms.Services/Service/ExpensesService.cs
Normal file
895
Marco.Pms.Services/Service/ExpensesService.cs
Normal file
@ -0,0 +1,895 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
using Marco.Pms.CacheHelper;
|
||||||
|
using Marco.Pms.DataAccess.Data;
|
||||||
|
using Marco.Pms.Model.DocumentManager;
|
||||||
|
using Marco.Pms.Model.Dtos.Expenses;
|
||||||
|
using Marco.Pms.Model.Employees;
|
||||||
|
using Marco.Pms.Model.Entitlements;
|
||||||
|
using Marco.Pms.Model.Expenses;
|
||||||
|
using Marco.Pms.Model.MongoDBModels;
|
||||||
|
using Marco.Pms.Model.Utilities;
|
||||||
|
using Marco.Pms.Model.ViewModels.Activities;
|
||||||
|
using Marco.Pms.Model.ViewModels.Expanses;
|
||||||
|
using Marco.Pms.Model.ViewModels.Master;
|
||||||
|
using Marco.Pms.Model.ViewModels.Projects;
|
||||||
|
using Marco.Pms.Services.Service.ServiceInterfaces;
|
||||||
|
using MarcoBMS.Services.Service;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace Marco.Pms.Services.Service
|
||||||
|
{
|
||||||
|
public class ExpensesService : IExpensesService
|
||||||
|
{
|
||||||
|
private readonly IDbContextFactory<ApplicationDbContext> _dbContextFactory;
|
||||||
|
private readonly ApplicationDbContext _context;
|
||||||
|
private readonly ILoggingService _logger;
|
||||||
|
private readonly S3UploadService _s3Service;
|
||||||
|
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||||
|
private readonly UpdateLogHelper _updateLogHelper;
|
||||||
|
private readonly IMapper _mapper;
|
||||||
|
private static readonly Guid Draft = Guid.Parse("297e0d8f-f668-41b5-bfea-e03b354251c8");
|
||||||
|
private static readonly string Collection = "ExpensesModificationLog";
|
||||||
|
public ExpensesService(
|
||||||
|
IDbContextFactory<ApplicationDbContext> dbContextFactory,
|
||||||
|
ApplicationDbContext context,
|
||||||
|
IServiceScopeFactory serviceScopeFactory,
|
||||||
|
UpdateLogHelper updateLogHelper,
|
||||||
|
ILoggingService logger,
|
||||||
|
S3UploadService s3Service,
|
||||||
|
IMapper mapper)
|
||||||
|
{
|
||||||
|
_dbContextFactory = dbContextFactory;
|
||||||
|
_context = context;
|
||||||
|
_logger = logger;
|
||||||
|
_serviceScopeFactory = serviceScopeFactory;
|
||||||
|
_updateLogHelper = updateLogHelper;
|
||||||
|
_s3Service = s3Service;
|
||||||
|
_mapper = mapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves a paginated list of expenses based on user permissions and optional filters.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filter">A URL-encoded JSON string containing filter criteria. See <see cref="ExpensesFilter"/>.</param>
|
||||||
|
/// <param name="pageSize">The number of records to return per page.</param>
|
||||||
|
/// <param name="pageNumber">The page number to retrieve.</param>
|
||||||
|
/// <returns>A paginated list of expenses.</returns>
|
||||||
|
public async Task<ApiResponse<object>> GetExpensesListAsync(Employee loggedInEmployee, Guid tenantId, string? filter, int pageSize, int pageNumber)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.LogInfo(
|
||||||
|
"Attempting to fetch expenses list for PageNumber: {PageNumber}, PageSize: {PageSize} with Filter: {Filter}",
|
||||||
|
pageNumber, pageSize, filter ?? "");
|
||||||
|
|
||||||
|
// 1. --- Get User Permissions ---
|
||||||
|
if (loggedInEmployee == null)
|
||||||
|
{
|
||||||
|
// This is an authentication/authorization issue. The user should be logged in.
|
||||||
|
_logger.LogWarning("Could not find an employee for the current logged-in user.");
|
||||||
|
return ApiResponse<object>.ErrorResponse("User not found or not authenticated.", 403);
|
||||||
|
}
|
||||||
|
Guid loggedInEmployeeId = loggedInEmployee.Id;
|
||||||
|
|
||||||
|
var hasViewSelfPermissionTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
using var scope = _serviceScopeFactory.CreateScope();
|
||||||
|
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
|
||||||
|
return await permissionService.HasPermission(PermissionsMaster.ExpenseViewSelf, loggedInEmployeeId);
|
||||||
|
});
|
||||||
|
|
||||||
|
var hasViewAllPermissionTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
using var scope = _serviceScopeFactory.CreateScope();
|
||||||
|
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
|
||||||
|
return await permissionService.HasPermission(PermissionsMaster.ExpenseViewAll, loggedInEmployeeId);
|
||||||
|
});
|
||||||
|
|
||||||
|
await Task.WhenAll(hasViewSelfPermissionTask, hasViewAllPermissionTask);
|
||||||
|
|
||||||
|
// 2. --- Build Base Query and Apply Permissions ---
|
||||||
|
// Start with a base IQueryable. Filters will be chained onto this.
|
||||||
|
var expensesQuery = _context.Expenses
|
||||||
|
.Include(e => e.ExpensesType)
|
||||||
|
.Include(e => e.Project)
|
||||||
|
.Include(e => e.PaidBy)
|
||||||
|
.ThenInclude(e => e!.JobRole)
|
||||||
|
.Include(e => e.PaymentMode)
|
||||||
|
.Include(e => e.Status)
|
||||||
|
.Include(e => e.CreatedBy)
|
||||||
|
.Where(e => e.TenantId == tenantId); // Always filter by TenantId first.
|
||||||
|
|
||||||
|
// Apply permission-based filtering BEFORE any other filters or pagination.
|
||||||
|
if (hasViewAllPermissionTask.Result)
|
||||||
|
{
|
||||||
|
// User has 'View All' permission, no initial restriction on who created the expense.
|
||||||
|
_logger.LogInfo("User {EmployeeId} has 'View All' permission.", loggedInEmployeeId);
|
||||||
|
}
|
||||||
|
else if (hasViewSelfPermissionTask.Result)
|
||||||
|
{
|
||||||
|
// User only has 'View Self' permission, so restrict the query to their own expenses.
|
||||||
|
_logger.LogInfo("User {EmployeeId} has 'View Self' permission. Restricting query to their expenses.", loggedInEmployeeId);
|
||||||
|
expensesQuery = expensesQuery.Where(e => e.CreatedById == loggedInEmployeeId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// User has neither required permission. Deny access.
|
||||||
|
_logger.LogWarning("Access DENIED for employee {EmployeeId} attempting to get expenses list.", loggedInEmployeeId);
|
||||||
|
return ApiResponse<object>.SuccessResponse(new List<ExpenseList>(), "You do not have permission to view any expenses.", 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. --- Deserialize Filter and Apply ---
|
||||||
|
ExpensesFilter? expenseFilter = TryDeserializeFilter(filter);
|
||||||
|
|
||||||
|
if (expenseFilter != null)
|
||||||
|
{
|
||||||
|
// CRITICAL FIX: Apply filters cumulatively using multiple `if` statements, not `if-else if`.
|
||||||
|
if (expenseFilter.StartDate.HasValue && expenseFilter.EndDate.HasValue)
|
||||||
|
{
|
||||||
|
expensesQuery = expensesQuery.Where(e => e.CreatedAt.Date >= expenseFilter.StartDate.Value.Date && e.CreatedAt.Date <= expenseFilter.EndDate.Value.Date);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expenseFilter.ProjectIds?.Any() == true)
|
||||||
|
{
|
||||||
|
expensesQuery = expensesQuery.Where(e => expenseFilter.ProjectIds.Contains(e.ProjectId));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expenseFilter.StatusIds?.Any() == true)
|
||||||
|
{
|
||||||
|
expensesQuery = expensesQuery.Where(e => expenseFilter.StatusIds.Contains(e.StatusId));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expenseFilter.PaidById?.Any() == true)
|
||||||
|
{
|
||||||
|
expensesQuery = expensesQuery.Where(e => expenseFilter.PaidById.Contains(e.PaidById));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only allow filtering by 'CreatedBy' if the user has 'View All' permission.
|
||||||
|
if (expenseFilter.CreatedByIds?.Any() == true && hasViewAllPermissionTask.Result)
|
||||||
|
{
|
||||||
|
expensesQuery = expensesQuery.Where(e => expenseFilter.CreatedByIds.Contains(e.CreatedById));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. --- Apply Ordering and Pagination ---
|
||||||
|
// This should be the last step before executing the query.
|
||||||
|
var paginatedQuery = expensesQuery
|
||||||
|
.OrderByDescending(e => e.CreatedAt)
|
||||||
|
.Skip((pageNumber - 1) * pageSize)
|
||||||
|
.Take(pageSize);
|
||||||
|
|
||||||
|
// 5. --- Execute Query and Map Results ---
|
||||||
|
var expensesList = await paginatedQuery.ToListAsync();
|
||||||
|
|
||||||
|
if (!expensesList.Any())
|
||||||
|
{
|
||||||
|
_logger.LogInfo("No expenses found matching the criteria for employee {EmployeeId}.", loggedInEmployeeId);
|
||||||
|
return ApiResponse<object>.SuccessResponse(new List<ExpenseList>(), "No expenses found for the given criteria.", 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = _mapper.Map<List<ExpenseList>>(expensesList);
|
||||||
|
|
||||||
|
// 6. --- Efficiently Fetch and Append 'Next Status' Information ---
|
||||||
|
var statusIds = expensesList.Select(e => e.StatusId).Distinct().ToList();
|
||||||
|
|
||||||
|
var statusMappings = await _context.ExpensesStatusMapping
|
||||||
|
.Include(sm => sm.NextStatus)
|
||||||
|
.Where(sm => statusIds.Contains(sm.StatusId))
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
// Use a Lookup for efficient O(1) mapping. This is much better than repeated `.Where()` in a loop.
|
||||||
|
var statusMapLookup = statusMappings.ToLookup(sm => sm.StatusId);
|
||||||
|
|
||||||
|
foreach (var expense in response)
|
||||||
|
{
|
||||||
|
if (expense.Status?.Id != null && statusMapLookup.Contains(expense.Status.Id))
|
||||||
|
{
|
||||||
|
expense.NextStatus = statusMapLookup[expense.Status.Id]
|
||||||
|
.Select(sm => _mapper.Map<ExpensesStatusMasterVM>(sm.NextStatus))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
expense.NextStatus = new List<ExpensesStatusMasterVM>(); // Ensure it's never null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. --- Return Final Success Response ---
|
||||||
|
var message = $"{response.Count} expense records fetched successfully.";
|
||||||
|
_logger.LogInfo(message);
|
||||||
|
return ApiResponse<object>.SuccessResponse(response, message, 200);
|
||||||
|
}
|
||||||
|
catch (DbUpdateException dbEx)
|
||||||
|
{
|
||||||
|
_logger.LogError(dbEx, "Databsae Exception occured while fetching list expenses");
|
||||||
|
return ApiResponse<object>.ErrorResponse("Databsae Exception", new
|
||||||
|
{
|
||||||
|
Message = dbEx.Message,
|
||||||
|
StackTrace = dbEx.StackTrace,
|
||||||
|
Source = dbEx.Source,
|
||||||
|
innerexcption = new
|
||||||
|
{
|
||||||
|
Message = dbEx.InnerException?.Message,
|
||||||
|
StackTrace = dbEx.InnerException?.StackTrace,
|
||||||
|
Source = dbEx.InnerException?.Source,
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error occured while fetching list expenses");
|
||||||
|
return ApiResponse<object>.ErrorResponse("Error Occured", new
|
||||||
|
{
|
||||||
|
Message = ex.Message,
|
||||||
|
StackTrace = ex.StackTrace,
|
||||||
|
Source = ex.Source,
|
||||||
|
innerexcption = new
|
||||||
|
{
|
||||||
|
Message = ex.InnerException?.Message,
|
||||||
|
StackTrace = ex.InnerException?.StackTrace,
|
||||||
|
Source = ex.InnerException?.Source,
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Get(int id)
|
||||||
|
{
|
||||||
|
return "value";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new expense entry along with its bill attachments.
|
||||||
|
/// This operation is transactional and performs validations and file uploads concurrently for optimal performance
|
||||||
|
/// by leveraging async/await without unnecessary thread-pool switching via Task.Run.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dto">The data transfer object containing expense details and attachments.</param>
|
||||||
|
/// <returns>An IActionResult indicating the result of the creation operation.</returns>
|
||||||
|
public async Task<ApiResponse<object>> CreateExpenseAsync(CreateExpensesDto dto, Employee loggedInEmployee, Guid tenantId)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Starting CreateExpense for Project {ProjectId}", dto.ProjectId);
|
||||||
|
// The entire operation is wrapped in a transaction to ensure data consistency.
|
||||||
|
await using var transaction = await _context.Database.BeginTransactionAsync();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 1. Authorization & Validation: Run all I/O-bound checks concurrently using factories for safety.
|
||||||
|
|
||||||
|
// PERMISSION CHECKS: Use IServiceScopeFactory for thread-safe access to scoped services.
|
||||||
|
var hasUploadPermissionTask = Task.Run(async () => // Task.Run is acceptable here to create a new scope, but let's do it cleaner.
|
||||||
|
{
|
||||||
|
using var scope = _serviceScopeFactory.CreateScope();
|
||||||
|
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
|
||||||
|
return await permissionService.HasPermission(PermissionsMaster.ExpenseUpload, loggedInEmployee.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
var hasProjectPermissionTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
using var scope = _serviceScopeFactory.CreateScope();
|
||||||
|
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
|
||||||
|
return await permissionService.HasProjectPermission(loggedInEmployee, dto.ProjectId);
|
||||||
|
});
|
||||||
|
|
||||||
|
// VALIDATION CHECKS: Use IDbContextFactory for thread-safe, parallel database queries.
|
||||||
|
// Each task gets its own DbContext instance.
|
||||||
|
var projectTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
|
return await dbContext.Projects.AsNoTracking().FirstOrDefaultAsync(p => p.Id == dto.ProjectId);
|
||||||
|
});
|
||||||
|
var paidByTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
|
return await dbContext.Employees.AsNoTracking().FirstOrDefaultAsync(e => e.Id == dto.PaidById);
|
||||||
|
});
|
||||||
|
var expenseTypeTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
|
return await dbContext.ExpensesTypeMaster.AsNoTracking().FirstOrDefaultAsync(et => et.Id == dto.ExpensesTypeId);
|
||||||
|
});
|
||||||
|
var paymentModeTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
|
return await dbContext.PaymentModeMatser.AsNoTracking().FirstOrDefaultAsync(pm => pm.Id == dto.PaymentModeId);
|
||||||
|
});
|
||||||
|
var statusMappingTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
|
return await dbContext.ExpensesStatusMapping
|
||||||
|
.Include(s => s.Status)
|
||||||
|
.Include(s => s.NextStatus)
|
||||||
|
.AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(es => es.StatusId == Draft && es.Status != null);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Await all prerequisite checks at once.
|
||||||
|
await Task.WhenAll(
|
||||||
|
hasUploadPermissionTask, hasProjectPermissionTask,
|
||||||
|
projectTask, expenseTypeTask, paymentModeTask, statusMappingTask, paidByTask
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. Aggregate and Check Results
|
||||||
|
if (!await hasUploadPermissionTask || !await hasProjectPermissionTask)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Access DENIED for employee {EmployeeId} on project {ProjectId}.", loggedInEmployee.Id, dto.ProjectId);
|
||||||
|
return ApiResponse<object>.ErrorResponse("Access Denied.", "You do not have permission to upload expenses for this project.", 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
var validationErrors = new List<string>();
|
||||||
|
var project = await projectTask;
|
||||||
|
var expenseType = await expenseTypeTask;
|
||||||
|
var paymentMode = await paymentModeTask;
|
||||||
|
var statusMapping = await statusMappingTask;
|
||||||
|
var paidBy = await paidByTask;
|
||||||
|
|
||||||
|
if (project == null) validationErrors.Add("Project not found.");
|
||||||
|
if (paidBy == null) validationErrors.Add("Paid by employee not found");
|
||||||
|
if (expenseType == null) validationErrors.Add("Expense Type not found.");
|
||||||
|
if (paymentMode == null) validationErrors.Add("Payment Mode not found.");
|
||||||
|
if (statusMapping == null) validationErrors.Add("Default status 'Draft' not found.");
|
||||||
|
|
||||||
|
if (validationErrors.Any())
|
||||||
|
{
|
||||||
|
await transaction.RollbackAsync();
|
||||||
|
var errorMessage = string.Join(" ", validationErrors);
|
||||||
|
_logger.LogWarning("Expense creation failed due to validation errors: {ValidationErrors}", errorMessage);
|
||||||
|
return ApiResponse<object>.ErrorResponse("Invalid input data.", errorMessage, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Entity Creation
|
||||||
|
var expense = _mapper.Map<Expenses>(dto);
|
||||||
|
expense.CreatedById = loggedInEmployee.Id;
|
||||||
|
expense.CreatedAt = DateTime.UtcNow;
|
||||||
|
expense.TenantId = tenantId;
|
||||||
|
expense.IsActive = true;
|
||||||
|
expense.StatusId = Draft;
|
||||||
|
|
||||||
|
_context.Expenses.Add(expense);
|
||||||
|
|
||||||
|
// 4. Process Attachments
|
||||||
|
if (dto.BillAttachments?.Any() ?? false)
|
||||||
|
{
|
||||||
|
await ProcessAndUploadAttachmentsAsync(dto.BillAttachments, expense, loggedInEmployee.Id, tenantId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Database Commit
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
// 6. Transaction Commit
|
||||||
|
await transaction.CommitAsync();
|
||||||
|
|
||||||
|
var response = _mapper.Map<ExpenseList>(expense);
|
||||||
|
response.PaidBy = _mapper.Map<BasicEmployeeVM>(paidBy);
|
||||||
|
response.Project = _mapper.Map<ProjectInfoVM>(project);
|
||||||
|
response.Status = _mapper.Map<ExpensesStatusMasterVM>(statusMapping!.Status);
|
||||||
|
response.NextStatus = _mapper.Map<List<ExpensesStatusMasterVM>>(statusMapping!.NextStatus);
|
||||||
|
response.PaymentMode = _mapper.Map<PaymentModeMatserVM>(paymentMode);
|
||||||
|
response.ExpensesType = _mapper.Map<ExpensesTypeMasterVM>(expenseType);
|
||||||
|
|
||||||
|
_logger.LogInfo("Successfully created Expense {ExpenseId} for Project {ProjectId}.", expense.Id, expense.ProjectId);
|
||||||
|
return ApiResponse<object>.SuccessResponse(response, "Expense created successfully.", 201);
|
||||||
|
}
|
||||||
|
catch (ArgumentException ex) // Catches bad Base64 from attachment pre-validation
|
||||||
|
{
|
||||||
|
await transaction.RollbackAsync();
|
||||||
|
_logger.LogError(ex, "Invalid argument during expense creation for project {ProjectId}.", dto.ProjectId);
|
||||||
|
return ApiResponse<object>.ErrorResponse("Invalid Request Data.", new
|
||||||
|
{
|
||||||
|
Message = ex.Message,
|
||||||
|
StackTrace = ex.StackTrace,
|
||||||
|
Source = ex.Source,
|
||||||
|
innerexcption = new
|
||||||
|
{
|
||||||
|
Message = ex.InnerException?.Message,
|
||||||
|
StackTrace = ex.InnerException?.StackTrace,
|
||||||
|
Source = ex.InnerException?.Source,
|
||||||
|
}
|
||||||
|
}, 400);
|
||||||
|
}
|
||||||
|
catch (Exception ex) // General-purpose catch for unexpected errors (e.g., S3 or DB connection failure)
|
||||||
|
{
|
||||||
|
await transaction.RollbackAsync();
|
||||||
|
_logger.LogError(ex, "An unhandled exception occurred while creating an expense for project {ProjectId}.", dto.ProjectId);
|
||||||
|
return ApiResponse<object>.ErrorResponse("An internal server error occurred.", new
|
||||||
|
{
|
||||||
|
Message = ex.Message,
|
||||||
|
StackTrace = ex.StackTrace,
|
||||||
|
Source = ex.Source,
|
||||||
|
innerexcption = new
|
||||||
|
{
|
||||||
|
Message = ex.InnerException?.Message,
|
||||||
|
StackTrace = ex.InnerException?.StackTrace,
|
||||||
|
Source = ex.InnerException?.Source,
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ApiResponse<object>> ChangeStatus(ExpenseRecordDto model, Employee loggedInEmployee, Guid tenantId)
|
||||||
|
{
|
||||||
|
|
||||||
|
var existingExpense = await _context.Expenses
|
||||||
|
.Include(e => e.ExpensesType)
|
||||||
|
.Include(e => e.Project)
|
||||||
|
.Include(e => e.PaidBy)
|
||||||
|
.ThenInclude(e => e!.JobRole)
|
||||||
|
.Include(e => e.PaymentMode)
|
||||||
|
.Include(e => e.Status)
|
||||||
|
.Include(e => e.CreatedBy)
|
||||||
|
.FirstOrDefaultAsync(e => e.Id == model.ExpenseId && e.StatusId != model.StatusId && e.TenantId == tenantId);
|
||||||
|
|
||||||
|
if (existingExpense == null)
|
||||||
|
{
|
||||||
|
return ApiResponse<object>.ErrorResponse("Expense not found", "Expense not found", 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
var statusMappingTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
|
return await dbContext.ExpensesStatusMapping
|
||||||
|
.Include(s => s.NextStatus)
|
||||||
|
.FirstOrDefaultAsync(s => s.StatusId == existingExpense.StatusId && s.NextStatus != null && s.NextStatusId == model.StatusId && s.TenantId == tenantId);
|
||||||
|
});
|
||||||
|
var statusPermissionMappingTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
|
return await dbContext.StatusPermissionMapping.Where(sp => sp.StatusId == model.StatusId).ToListAsync();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Await all prerequisite checks at once.
|
||||||
|
await Task.WhenAll(statusMappingTask, statusPermissionMappingTask);
|
||||||
|
|
||||||
|
var statusMapping = await statusMappingTask;
|
||||||
|
var statusPermissions = await statusPermissionMappingTask;
|
||||||
|
if (statusMapping == null)
|
||||||
|
{
|
||||||
|
return ApiResponse<object>.ErrorResponse("There is no follow-up status for currect status");
|
||||||
|
}
|
||||||
|
if (statusPermissions.Any())
|
||||||
|
{
|
||||||
|
foreach (var sp in statusPermissions)
|
||||||
|
{
|
||||||
|
using var scope = _serviceScopeFactory.CreateScope();
|
||||||
|
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
|
||||||
|
|
||||||
|
if (!await permissionService.HasPermission(sp.PermissionId, loggedInEmployee.Id))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Access DENIED for employee {EmployeeId} attempting to change status of expense from status {StatusId} to {NextStatusId}",
|
||||||
|
loggedInEmployee.Id, statusMapping.StatusId, statusMapping.NextStatusId);
|
||||||
|
return ApiResponse<object>.ErrorResponse("You do not have permission", "Access Denied", 403);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (existingExpense.CreatedById != loggedInEmployee.Id)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Access DENIED for employee {EmployeeId} attempting to change status of expense from status {StatusId} to {NextStatusId}",
|
||||||
|
loggedInEmployee.Id, statusMapping.StatusId, statusMapping.NextStatusId);
|
||||||
|
return ApiResponse<object>.ErrorResponse("You do not have permission", "Access Denied", 403);
|
||||||
|
}
|
||||||
|
var existingEntity = _updateLogHelper.EntityToBsonDocument(existingExpense);
|
||||||
|
|
||||||
|
existingExpense.StatusId = statusMapping.NextStatusId;
|
||||||
|
existingExpense.Status = statusMapping.NextStatus;
|
||||||
|
|
||||||
|
_context.ExpenseLogs.Add(new ExpenseLog
|
||||||
|
{
|
||||||
|
ExpenseId = existingExpense.Id,
|
||||||
|
Action = statusMapping.NextStatus!.Name,
|
||||||
|
UpdatedById = loggedInEmployee.Id,
|
||||||
|
Comment = model.Comment
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
catch (DbUpdateConcurrencyException dbEx)
|
||||||
|
{
|
||||||
|
_logger.LogError(dbEx, "Error occured while update status of expanse.");
|
||||||
|
return ApiResponse<object>.ErrorResponse("Error occured while update status of expanse.", new
|
||||||
|
{
|
||||||
|
Message = dbEx.Message,
|
||||||
|
StackTrace = dbEx.StackTrace,
|
||||||
|
Source = dbEx.Source,
|
||||||
|
innerexcption = new
|
||||||
|
{
|
||||||
|
Message = dbEx.InnerException?.Message,
|
||||||
|
StackTrace = dbEx.InnerException?.StackTrace,
|
||||||
|
Source = dbEx.InnerException?.Source,
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var updateLog = new UpdateLogsObject
|
||||||
|
{
|
||||||
|
EntityId = existingExpense.Id.ToString(),
|
||||||
|
UpdatedById = loggedInEmployee.Id.ToString(),
|
||||||
|
OldObject = existingEntity,
|
||||||
|
UpdatedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
var mongoDBTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await _updateLogHelper.PushToUpdateLogsAsync(updateLog, Collection);
|
||||||
|
});
|
||||||
|
|
||||||
|
var getNextStatusTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
|
return await dbContext.ExpensesStatusMapping
|
||||||
|
.Include(s => s.NextStatus)
|
||||||
|
.FirstOrDefaultAsync(s => s.StatusId == existingExpense.StatusId && s.NextStatus != null && s.TenantId == tenantId);
|
||||||
|
});
|
||||||
|
|
||||||
|
await Task.WhenAll(mongoDBTask, getNextStatusTask);
|
||||||
|
|
||||||
|
var getNextStatus = await getNextStatusTask;
|
||||||
|
|
||||||
|
var response = _mapper.Map<ExpenseList>(existingExpense);
|
||||||
|
if (getNextStatus != null)
|
||||||
|
{
|
||||||
|
response.NextStatus = _mapper.Map<List<ExpensesStatusMasterVM>>(getNextStatus.NextStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiResponse<object>.SuccessResponse(response);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error occured while Saving old entity in mongoDb");
|
||||||
|
return ApiResponse<object>.ErrorResponse("Error occured while update status of expanse.", new
|
||||||
|
{
|
||||||
|
Message = ex.Message,
|
||||||
|
StackTrace = ex.StackTrace,
|
||||||
|
Source = ex.Source,
|
||||||
|
innerexcption = new
|
||||||
|
{
|
||||||
|
Message = ex.InnerException?.Message,
|
||||||
|
StackTrace = ex.InnerException?.StackTrace,
|
||||||
|
Source = ex.InnerException?.Source,
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Changes the status of an expense record, performing validation, permission checks, and logging.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="model">The DTO containing the expense ID and the target status ID.</param>
|
||||||
|
/// <param name="loggedInEmployee">The employee performing the action.</param>
|
||||||
|
/// <param name="tenantId">The ID of the tenant owning the expense.</param>
|
||||||
|
/// <returns>An ApiResponse containing the updated expense details or an error.</returns>
|
||||||
|
public async Task<ApiResponse<object>> ChangeStatusAsync(ExpenseRecordDto model, Employee loggedInEmployee, Guid tenantId)
|
||||||
|
{
|
||||||
|
// --- 1. Fetch Existing Expense ---
|
||||||
|
// We include all related entities needed for the final response mapping to avoid multiple database trips.
|
||||||
|
// The query also ensures we don't process a request if the status is already the one requested.
|
||||||
|
var existingExpense = await _context.Expenses
|
||||||
|
.Include(e => e.ExpensesType)
|
||||||
|
.Include(e => e.Project)
|
||||||
|
.Include(e => e.PaidBy)
|
||||||
|
.ThenInclude(e => e!.JobRole)
|
||||||
|
.Include(e => e.PaymentMode)
|
||||||
|
.Include(e => e.Status)
|
||||||
|
.Include(e => e.CreatedBy)
|
||||||
|
.FirstOrDefaultAsync(e => e.Id == model.ExpenseId && e.StatusId != model.StatusId && e.TenantId == tenantId);
|
||||||
|
|
||||||
|
if (existingExpense == null)
|
||||||
|
{
|
||||||
|
// Use structured logging for better searchability.
|
||||||
|
_logger.LogWarning("Attempted to change status for a non-existent or already-updated expense. ExpenseId: {ExpenseId}, TenantId: {TenantId}", model.ExpenseId, tenantId);
|
||||||
|
return ApiResponse<object>.ErrorResponse("Expense not found or status is already set.", "Expense not found", 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInfo("Initiating status change for ExpenseId: {ExpenseId} from StatusId: {OldStatusId} to {NewStatusId}",
|
||||||
|
existingExpense.Id, existingExpense.StatusId, model.StatusId);
|
||||||
|
|
||||||
|
// --- 2. Concurrently Check Prerequisites ---
|
||||||
|
// We run status validation and permission fetching in parallel for efficiency.
|
||||||
|
// Using Task.Run with an async lambda is the standard way to start a concurrent,
|
||||||
|
// CPU- or I/O-bound operation on a background thread.
|
||||||
|
|
||||||
|
// Task to validate if the requested status change is a valid transition.
|
||||||
|
var statusMappingTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
// 'await using' ensures the DbContext created by the factory is properly disposed
|
||||||
|
// within the scope of this background task.
|
||||||
|
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
|
return await dbContext.ExpensesStatusMapping
|
||||||
|
.Include(s => s.NextStatus)
|
||||||
|
.FirstOrDefaultAsync(s => s.StatusId == existingExpense.StatusId && s.NextStatusId == model.StatusId && s.TenantId == tenantId);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Task to fetch all permissions required for the *target* status.
|
||||||
|
var statusPermissionMappingTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
|
return await dbContext.StatusPermissionMapping
|
||||||
|
.Where(sp => sp.StatusId == model.StatusId && sp.TenantId == tenantId)
|
||||||
|
.ToListAsync();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Await both tasks to complete concurrently.
|
||||||
|
await Task.WhenAll(statusMappingTask, statusPermissionMappingTask);
|
||||||
|
|
||||||
|
// Now you can safely get the results.
|
||||||
|
var statusMapping = await statusMappingTask;
|
||||||
|
var statusPermissions = await statusPermissionMappingTask;
|
||||||
|
|
||||||
|
// --- 3. Validate Status Transition and Permissions ---
|
||||||
|
if (statusMapping == null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Invalid status transition attempted for ExpenseId: {ExpenseId}. From StatusId: {FromStatusId} to {ToStatusId}",
|
||||||
|
existingExpense.Id, existingExpense.StatusId, model.StatusId);
|
||||||
|
return ApiResponse<object>.ErrorResponse("This status change is not allowed.", "Invalid Transition", 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check permissions. The logic is:
|
||||||
|
// 1. If the target status has specific permissions defined, the user must have at least one of them.
|
||||||
|
// 2. If no permissions are defined for the target status, only the original creator of the expense can change it.
|
||||||
|
bool hasPermission = false;
|
||||||
|
if (statusPermissions.Any())
|
||||||
|
{
|
||||||
|
// Using a scope to resolve scoped services like PermissionServices.
|
||||||
|
using var scope = _serviceScopeFactory.CreateScope();
|
||||||
|
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
|
||||||
|
foreach (var sp in statusPermissions)
|
||||||
|
{
|
||||||
|
if (await permissionService.HasPermission(sp.PermissionId, loggedInEmployee.Id))
|
||||||
|
{
|
||||||
|
hasPermission = true;
|
||||||
|
break; // User has one of the required permissions, no need to check further.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (existingExpense.CreatedById == loggedInEmployee.Id)
|
||||||
|
{
|
||||||
|
// Fallback: If no permissions are required for the status, allow the creator to make the change.
|
||||||
|
hasPermission = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasPermission)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Access DENIED for EmployeeId: {EmployeeId} attempting to change status of ExpenseId: {ExpenseId} to StatusId: {NewStatusId}",
|
||||||
|
loggedInEmployee.Id, existingExpense.Id, model.StatusId);
|
||||||
|
return ApiResponse<object>.ErrorResponse("You do not have the required permissions to perform this action.", "Access Denied", 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 4. Update Expense and Add Log (in a transaction) ---
|
||||||
|
var existingEntityBson = _updateLogHelper.EntityToBsonDocument(existingExpense); // Capture state for audit log BEFORE changes.
|
||||||
|
|
||||||
|
existingExpense.StatusId = statusMapping.NextStatusId;
|
||||||
|
existingExpense.Status = statusMapping.NextStatus; // Assigning the included entity for the response mapping.
|
||||||
|
|
||||||
|
_context.ExpenseLogs.Add(new ExpenseLog
|
||||||
|
{
|
||||||
|
ExpenseId = existingExpense.Id,
|
||||||
|
Action = $"Status changed to '{statusMapping.NextStatus!.Name}'",
|
||||||
|
UpdatedById = loggedInEmployee.Id,
|
||||||
|
Comment = model.Comment,
|
||||||
|
TenantId = tenantId
|
||||||
|
});
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
_logger.LogInfo("Successfully updated status for ExpenseId: {ExpenseId} to StatusId: {NewStatusId}", existingExpense.Id, existingExpense.StatusId);
|
||||||
|
}
|
||||||
|
catch (DbUpdateConcurrencyException dbEx)
|
||||||
|
{
|
||||||
|
// This error occurs if the record was modified by someone else after we fetched it.
|
||||||
|
_logger.LogError(dbEx, "Concurrency conflict while updating status for ExpenseId: {ExpenseId}. The record may have been modified by another user.", existingExpense.Id);
|
||||||
|
return ApiResponse<object>.ErrorResponse("The expense was modified by another user. Please refresh and try again.", "Concurrency Error", 409); // 409 Conflict is appropriate
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 5. Perform Post-Save Actions (Audit Log and Fetching Next State for UI) ---
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Task to save the detailed audit log to a separate system (e.g., MongoDB).
|
||||||
|
var mongoDBTask = _updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject
|
||||||
|
{
|
||||||
|
EntityId = existingExpense.Id.ToString(),
|
||||||
|
UpdatedById = loggedInEmployee.Id.ToString(),
|
||||||
|
OldObject = existingEntityBson,
|
||||||
|
UpdatedAt = DateTime.UtcNow
|
||||||
|
}, Collection);
|
||||||
|
|
||||||
|
// Task to get all possible next statuses from the *new* current state to help the UI.
|
||||||
|
// NOTE: This now fetches a list of all possible next states, which is more useful for a UI.
|
||||||
|
var getNextStatusesTask = _dbContextFactory.CreateDbContextAsync().ContinueWith(t =>
|
||||||
|
{
|
||||||
|
var dbContext = t.Result;
|
||||||
|
return dbContext.ExpensesStatusMapping
|
||||||
|
.Include(s => s.NextStatus)
|
||||||
|
.Where(s => s.StatusId == existingExpense.StatusId && s.NextStatus != null && s.TenantId == tenantId)
|
||||||
|
.Select(s => s.NextStatus) // Select only the status object
|
||||||
|
.ToListAsync()
|
||||||
|
.ContinueWith(res =>
|
||||||
|
{
|
||||||
|
dbContext.Dispose(); // Ensure the context is disposed
|
||||||
|
return res.Result;
|
||||||
|
});
|
||||||
|
}).Unwrap();
|
||||||
|
|
||||||
|
await Task.WhenAll(mongoDBTask, getNextStatusesTask);
|
||||||
|
|
||||||
|
var nextPossibleStatuses = await getNextStatusesTask;
|
||||||
|
|
||||||
|
var response = _mapper.Map<ExpenseList>(existingExpense);
|
||||||
|
if (nextPossibleStatuses != null)
|
||||||
|
{
|
||||||
|
// The response DTO should have a property like: public List<ExpensesStatusMasterVM> NextAvailableStatuses { get; set; }
|
||||||
|
response.NextStatus = _mapper.Map<List<ExpensesStatusMasterVM>>(nextPossibleStatuses);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiResponse<object>.SuccessResponse(response);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// This catch block handles errors from post-save operations like MongoDB logging.
|
||||||
|
// The primary update was successful, but we must log this failure.
|
||||||
|
_logger.LogError(ex, "Error occurred during post-save operations for ExpenseId: {ExpenseId} (e.g., audit logging). The primary status change was successful.", existingExpense.Id);
|
||||||
|
|
||||||
|
// We can still return a success response because the main operation succeeded,
|
||||||
|
// but we should not block the user for a failed audit log.
|
||||||
|
// Alternatively, if audit logging is critical, you could return an error.
|
||||||
|
// Here, we choose to return success but log the ancillary failure.
|
||||||
|
var response = _mapper.Map<ExpenseList>(existingExpense);
|
||||||
|
return ApiResponse<object>.SuccessResponse(response, "Status updated, but a post-processing error occurred.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public async Task<ApiResponse<object>> UpdateExpanseAsync(Guid id, UpdateExpensesDto model, Employee loggedInEmployee, Guid tenantId)
|
||||||
|
{
|
||||||
|
var exsitingExpense = await _context.Expenses.FirstOrDefaultAsync(e => e.Id == model.Id && e.TenantId == tenantId);
|
||||||
|
|
||||||
|
|
||||||
|
if (exsitingExpense == null)
|
||||||
|
{
|
||||||
|
return ApiResponse<object>.ErrorResponse("Expense not found", "Expense not found", 404);
|
||||||
|
}
|
||||||
|
_mapper.Map(model, exsitingExpense);
|
||||||
|
_context.Entry(exsitingExpense).State = EntityState.Modified;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
_logger.LogInfo("Successfully updated project {ProjectId} by user {UserId}.", id, loggedInEmployee.Id);
|
||||||
|
}
|
||||||
|
catch (DbUpdateConcurrencyException ex)
|
||||||
|
{
|
||||||
|
// --- Step 3: Handle Concurrency Conflicts ---
|
||||||
|
// This happens if another user modified the project after we fetched it.
|
||||||
|
_logger.LogError(ex, "Concurrency conflict while updating project {ProjectId} ", id);
|
||||||
|
return ApiResponse<object>.ErrorResponse("Conflict occurred.", "This project has been modified by someone else. Please refresh and try again.", 409);
|
||||||
|
}
|
||||||
|
var response = _mapper.Map<ExpenseList>(exsitingExpense);
|
||||||
|
return ApiResponse<object>.SuccessResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Delete(int id)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
#region =================================================================== Helper Functions ===================================================================
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deserializes the filter string, handling multiple potential formats (e.g., direct JSON vs. escaped JSON string).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filter">The JSON filter string from the request.</param>
|
||||||
|
/// <returns>An <see cref="ExpensesFilter"/> object or null if deserialization fails.</returns>
|
||||||
|
private ExpensesFilter? TryDeserializeFilter(string? filter)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(filter))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
|
||||||
|
ExpensesFilter? expenseFilter = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// First, try to deserialize directly. This is the expected case (e.g., from a web client).
|
||||||
|
expenseFilter = JsonSerializer.Deserialize<ExpensesFilter>(filter, options);
|
||||||
|
}
|
||||||
|
catch (JsonException ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "[{MethodName}] Failed to directly deserialize filter. Attempting to unescape and re-parse. Filter: {Filter}", nameof(TryDeserializeFilter), filter);
|
||||||
|
|
||||||
|
// If direct deserialization fails, it might be an escaped string (common with tools like Postman or some mobile clients).
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Unescape the string first, then deserialize the result.
|
||||||
|
string unescapedJsonString = JsonSerializer.Deserialize<string>(filter, options) ?? "";
|
||||||
|
if (!string.IsNullOrWhiteSpace(unescapedJsonString))
|
||||||
|
{
|
||||||
|
expenseFilter = JsonSerializer.Deserialize<ExpensesFilter>(unescapedJsonString, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (JsonException ex1)
|
||||||
|
{
|
||||||
|
// If both attempts fail, log the final error and return null.
|
||||||
|
_logger.LogError(ex1, "[{MethodName}] All attempts to deserialize the filter failed. Filter will be ignored. Filter: {Filter}", nameof(TryDeserializeFilter), filter);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return expenseFilter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Processes and uploads attachments concurrently, then adds the resulting entities to the main DbContext.
|
||||||
|
/// </summary>
|
||||||
|
private async Task ProcessAndUploadAttachmentsAsync(IEnumerable<FileUploadModel> attachments, Expenses expense, Guid employeeId, Guid tenantId)
|
||||||
|
{
|
||||||
|
// Pre-validate all attachments to fail fast before any uploads.
|
||||||
|
foreach (var attachment in attachments)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(attachment.Base64Data) || !_s3Service.IsBase64String(attachment.Base64Data))
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"Invalid or missing Base64 data for attachment: {attachment.FileName ?? "N/A"}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var batchId = Guid.NewGuid();
|
||||||
|
|
||||||
|
// Create a list of tasks to be executed concurrently.
|
||||||
|
var processingTasks = attachments.Select(attachment =>
|
||||||
|
ProcessSingleAttachmentAsync(attachment, expense, employeeId, tenantId, batchId)
|
||||||
|
).ToList();
|
||||||
|
|
||||||
|
var results = await Task.WhenAll(processingTasks);
|
||||||
|
|
||||||
|
// This part is thread-safe as it runs after all concurrent tasks are complete.
|
||||||
|
foreach (var (document, billAttachment) in results)
|
||||||
|
{
|
||||||
|
_context.Documents.Add(document);
|
||||||
|
_context.BillAttachments.Add(billAttachment);
|
||||||
|
}
|
||||||
|
_logger.LogInfo("{AttachmentCount} attachments processed and staged for saving.", results.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles the logic for a single attachment: upload to S3 and create corresponding entities.
|
||||||
|
/// </summary>
|
||||||
|
private async Task<(Document document, BillAttachments billAttachment)> ProcessSingleAttachmentAsync(
|
||||||
|
FileUploadModel attachment, Expenses expense, Guid employeeId, Guid tenantId, Guid batchId)
|
||||||
|
{
|
||||||
|
var base64Data = attachment.Base64Data!.Contains(',') ? attachment.Base64Data[(attachment.Base64Data.IndexOf(",") + 1)..] : attachment.Base64Data;
|
||||||
|
var fileType = _s3Service.GetContentTypeFromBase64(base64Data);
|
||||||
|
var fileName = _s3Service.GenerateFileName(fileType, expense.Id, "Expense");
|
||||||
|
var objectKey = $"tenant-{tenantId}/project-{expense.ProjectId}/Expenses/{fileName}";
|
||||||
|
|
||||||
|
// Await the I/O-bound upload operation directly.
|
||||||
|
await _s3Service.UploadFileAsync(base64Data, fileType, objectKey);
|
||||||
|
_logger.LogInfo("Uploaded file to S3 with key: {ObjectKey}", objectKey);
|
||||||
|
|
||||||
|
return CreateAttachmentEntities(batchId, expense.Id, employeeId, tenantId, objectKey, attachment);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A private static helper method to create Document and BillAttachment entities.
|
||||||
|
/// This remains unchanged as it's a pure data-shaping method.
|
||||||
|
/// </summary>
|
||||||
|
private static (Document document, BillAttachments billAttachment) CreateAttachmentEntities(
|
||||||
|
Guid batchId, Guid expenseId, Guid uploadedById, Guid tenantId, string s3Key, FileUploadModel attachmentDto)
|
||||||
|
{
|
||||||
|
var document = new Document
|
||||||
|
{
|
||||||
|
BatchId = batchId,
|
||||||
|
UploadedById = uploadedById,
|
||||||
|
FileName = attachmentDto.FileName ?? "",
|
||||||
|
ContentType = attachmentDto.ContentType ?? "",
|
||||||
|
S3Key = s3Key,
|
||||||
|
FileSize = attachmentDto.FileSize,
|
||||||
|
UploadedAt = DateTime.UtcNow,
|
||||||
|
TenantId = tenantId
|
||||||
|
};
|
||||||
|
var billAttachment = new BillAttachments { Document = document, ExpensesId = expenseId, TenantId = tenantId };
|
||||||
|
return (document, billAttachment);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
using Marco.Pms.Model.Dtos.Expenses;
|
||||||
|
using Marco.Pms.Model.Employees;
|
||||||
|
using Marco.Pms.Model.Utilities;
|
||||||
|
|
||||||
|
namespace Marco.Pms.Services.Service.ServiceInterfaces
|
||||||
|
{
|
||||||
|
public interface IExpensesService
|
||||||
|
{
|
||||||
|
Task<ApiResponse<object>> GetExpensesListAsync(Employee loggedInEmployee, Guid tenantId, string? filter, int pageSize, int pageNumber);
|
||||||
|
Task<ApiResponse<object>> CreateExpenseAsync(CreateExpensesDto dto, Employee loggedInEmployee, Guid tenantId);
|
||||||
|
Task<ApiResponse<object>> ChangeStatusAsync(ExpenseRecordDto model, Employee loggedInEmployee, Guid tenantId);
|
||||||
|
Task<ApiResponse<object>> UpdateExpanseAsync(Guid id, UpdateExpensesDto model, Employee loggedInEmployee, Guid tenantId);
|
||||||
|
}
|
||||||
|
}
|
@ -46,8 +46,9 @@
|
|||||||
"Region": "us-east-1",
|
"Region": "us-east-1",
|
||||||
"BucketName": "testenv-marco-pms-documents"
|
"BucketName": "testenv-marco-pms-documents"
|
||||||
},
|
},
|
||||||
"MongoDB": {
|
"MongoDB": {
|
||||||
"SerilogDatabaseUrl": "mongodb://localhost:27017/DotNetLogs",
|
"SerilogDatabaseUrl": "mongodb://localhost:27017/DotNetLogs",
|
||||||
"ConnectionString": "mongodb://localhost:27017/MarcoBMS_Caches?socketTimeoutMS=500&serverSelectionTimeoutMS=500&connectTimeoutMS=500"
|
"ConnectionString": "mongodb://localhost:27017/MarcoBMS_Caches?socketTimeoutMS=500&serverSelectionTimeoutMS=500&connectTimeoutMS=500",
|
||||||
}
|
"ModificationConnectionString": "mongodb://localhost:27017/ModificationLog?socketTimeoutMS=500&serverSelectionTimeoutMS=500&connectTimeoutMS=500"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user