Compare commits

...

18 Commits

Author SHA1 Message Date
70aa5121fa Added the Finance in mobile menu 2025-10-15 17:43:58 +05:30
2e299c6152 Added the expense related dashboards 2025-10-15 13:00:17 +05:30
c13dec9052 Added the payement adjustment heads in master dropdown 2025-10-15 11:26:36 +05:30
8c305872a0 Added the CRUD API for Payment Adjustment heads 2025-10-15 11:24:48 +05:30
302185808e Added the Payment Adjustment Head table also new API of get Payment Adjustment Head lists as well added the forgin key in received payment table and API 2025-10-14 17:57:37 +05:30
4dc37cb0ac Added the projectID filter in get invoice list API 2025-10-14 16:55:46 +05:30
e80728805b Added the comment in Received Invoice Payment and added provide all project flag in get basic project 2025-10-14 15:34:38 +05:30
4434ea971f Added file name and content type in invoice attachment view models 2025-10-14 14:29:57 +05:30
5a9b06cca6 Added the update invoice API 2025-10-14 14:24:59 +05:30
47bb49fac6 Merge pull request 'Added the organization name in record response object' (#145) from Ashutosh_Bug#1498 into Collection_Management
Reviewed-on: #145
2025-10-14 06:07:09 +00:00
5ce95f7e23 Added the organization name in record response object 2025-10-14 11:36:32 +05:30
636dd48ad6 Change response for invoice number check 2025-10-14 11:29:04 +05:30
62688d508f Added the add comment API and get Details API 2025-10-14 11:24:35 +05:30
c92e71b292 Added the new API to markAsCompletedthe invoice 2025-10-13 21:08:11 +05:30
2b19890b53 Added the create payment received invoice API 2025-10-13 20:44:38 +05:30
5ff87cd870 Added the get list of invoices API 2025-10-13 19:59:18 +05:30
b30369baa5 Added the new API to create invoice 2025-10-13 18:35:57 +05:30
4684b438f6 Added the invoice related tables 2025-10-13 18:35:26 +05:30
39 changed files with 36000 additions and 269 deletions

View File

@ -1,6 +1,7 @@
using Marco.Pms.Model.Activities; using Marco.Pms.Model.Activities;
using Marco.Pms.Model.AttendanceModule; using Marco.Pms.Model.AttendanceModule;
using Marco.Pms.Model.Authentication; using Marco.Pms.Model.Authentication;
using Marco.Pms.Model.Collection;
using Marco.Pms.Model.Directory; using Marco.Pms.Model.Directory;
using Marco.Pms.Model.DocumentManager; using Marco.Pms.Model.DocumentManager;
using Marco.Pms.Model.Employees; using Marco.Pms.Model.Employees;
@ -133,6 +134,13 @@ namespace Marco.Pms.DataAccess.Data
public DbSet<ProjectServiceMapping> ProjectServiceMappings { get; set; } public DbSet<ProjectServiceMapping> ProjectServiceMappings { get; set; }
public DbSet<ProjectOrgMapping> ProjectOrgMappings { get; set; } public DbSet<ProjectOrgMapping> ProjectOrgMappings { get; set; }
// Collection
public DbSet<Invoice> Invoices { get; set; }
public DbSet<InvoiceComment> InvoiceComments { get; set; }
public DbSet<InvoiceAttachment> InvoiceAttachments { get; set; }
public DbSet<ReceivedInvoicePayment> ReceivedInvoicePayments { get; set; }
public DbSet<PaymentAdjustmentHead> PaymentAdjustmentHeads { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
@ -775,6 +783,65 @@ namespace Marco.Pms.DataAccess.Data
} }
); );
modelBuilder.Entity<PaymentAdjustmentHead>().HasData(
new PaymentAdjustmentHead
{
Id = Guid.Parse("dbdc047f-a2d2-4db0-b0e6-b9d9f923a0f1"),
Name = "Advance payment",
Description = "An advance payment is a sum paid before receiving goods or services, often to secure a transaction or cover initial costs.",
IsActive = true,
TenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26")
},
new PaymentAdjustmentHead
{
Id = Guid.Parse("66c3c241-8b52-4327-a5ad-c1faf102583e"),
Name = "Base Amount",
Description = "The base amount refers to the principal sum or original value used as a reference in financial calculations, excluding taxes, fees, or additional charges.",
IsActive = true,
TenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26")
},
new PaymentAdjustmentHead
{
Id = Guid.Parse("0d70cb2e-827e-44fc-90a5-c2c55ba51ba9"),
Name = "Tax Deducted at Source (TDS)",
Description = "TDS, or Tax Deducted at Source, is a system under the Indian Income Tax Act where tax is deducted at the point of income generation—such as salary, interest, or rent—and remitted to the government to prevent tax evasion and ensure timely collection.",
IsActive = true,
TenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26")
},
new PaymentAdjustmentHead
{
Id = Guid.Parse("95f35acd-d979-4177-91ea-fd03a00e49ff"),
Name = "Retention",
Description = "Retention refers to a company's ability to keep customers, employees, or profits over time, commonly measured as a percentage and critical for long-term business sustainability and growth.",
IsActive = true,
TenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26")
},
new PaymentAdjustmentHead
{
Id = Guid.Parse("3f09b19a-8d45-4cf2-be27-f4f09b38b9f7"),
Name = "Tax",
Description = "Tax is a mandatory financial charge imposed by a government on individuals or entities to fund public services and government operations, without direct benefit to the taxpayer.",
IsActive = true,
TenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26")
},
new PaymentAdjustmentHead
{
Id = Guid.Parse("ec5e6a5f-ce62-44e5-8911-8426bbb4dde8"),
Name = "Penalty",
Description = "A penalty in the context of taxation is a financial sanction imposed by the government on individuals or entities for non-compliance with tax laws, such as late filing, underreporting income, or failure to pay taxes, and is typically calculated as a percentage of the tax due or a fixed amount.",
IsActive = true,
TenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26")
},
new PaymentAdjustmentHead
{
Id = Guid.Parse("50584332-1cb7-4359-9721-c8ea35040881"),
Name = "Utility fees",
Description = "Utility fees are recurring charges for essential services such as electricity, water, gas, sewage, waste disposal, internet, and telecommunications, typically based on usage and necessary for operating a home or business.",
IsActive = true,
TenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26")
}
);
modelBuilder.Entity<EntityTypeMaster>().HasData( modelBuilder.Entity<EntityTypeMaster>().HasData(
new EntityTypeMaster new EntityTypeMaster
{ {

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,256 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Marco.Pms.DataAccess.Migrations
{
/// <inheritdoc />
public partial class Added_Collection_Related_Tables : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Invoices",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
Title = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Description = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
InvoiceNumber = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
ProjectId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
InvoiceDate = table.Column<DateTime>(type: "datetime(6)", nullable: false),
ClientSubmitedDate = table.Column<DateTime>(type: "datetime(6)", nullable: false),
ExceptedPaymentDate = table.Column<DateTime>(type: "datetime(6)", nullable: false),
Amount = table.Column<double>(type: "double", nullable: false),
IsActive = table.Column<bool>(type: "tinyint(1)", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
CreatedById = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
UpdatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: true),
UpdatedById = table.Column<Guid>(type: "char(36)", nullable: true, collation: "ascii_general_ci"),
TenantId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci")
},
constraints: table =>
{
table.PrimaryKey("PK_Invoices", x => x.Id);
table.ForeignKey(
name: "FK_Invoices_Employees_CreatedById",
column: x => x.CreatedById,
principalTable: "Employees",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Invoices_Employees_UpdatedById",
column: x => x.UpdatedById,
principalTable: "Employees",
principalColumn: "Id");
table.ForeignKey(
name: "FK_Invoices_Projects_ProjectId",
column: x => x.ProjectId,
principalTable: "Projects",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Invoices_Tenants_TenantId",
column: x => x.TenantId,
principalTable: "Tenants",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "InvoiceAttachments",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
InvoiceId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
DocumentId = 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_InvoiceAttachments", x => x.Id);
table.ForeignKey(
name: "FK_InvoiceAttachments_Documents_DocumentId",
column: x => x.DocumentId,
principalTable: "Documents",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_InvoiceAttachments_Invoices_InvoiceId",
column: x => x.InvoiceId,
principalTable: "Invoices",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_InvoiceAttachments_Tenants_TenantId",
column: x => x.TenantId,
principalTable: "Tenants",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "InvoiceComments",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
Comment = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
CreatedById = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
InvoiceId = 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_InvoiceComments", x => x.Id);
table.ForeignKey(
name: "FK_InvoiceComments_Employees_CreatedById",
column: x => x.CreatedById,
principalTable: "Employees",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_InvoiceComments_Invoices_InvoiceId",
column: x => x.InvoiceId,
principalTable: "Invoices",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_InvoiceComments_Tenants_TenantId",
column: x => x.TenantId,
principalTable: "Tenants",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "ReceivedInvoicePayments",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
InvoiceId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
PaymentReceivedDate = table.Column<DateTime>(type: "datetime(6)", nullable: false),
TransactionId = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Amount = table.Column<double>(type: "double", nullable: false),
IsActive = table.Column<bool>(type: "tinyint(1)", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
CreatedById = 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_ReceivedInvoicePayments", x => x.Id);
table.ForeignKey(
name: "FK_ReceivedInvoicePayments_Employees_CreatedById",
column: x => x.CreatedById,
principalTable: "Employees",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_ReceivedInvoicePayments_Invoices_InvoiceId",
column: x => x.InvoiceId,
principalTable: "Invoices",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_ReceivedInvoicePayments_Tenants_TenantId",
column: x => x.TenantId,
principalTable: "Tenants",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateIndex(
name: "IX_InvoiceAttachments_DocumentId",
table: "InvoiceAttachments",
column: "DocumentId");
migrationBuilder.CreateIndex(
name: "IX_InvoiceAttachments_InvoiceId",
table: "InvoiceAttachments",
column: "InvoiceId");
migrationBuilder.CreateIndex(
name: "IX_InvoiceAttachments_TenantId",
table: "InvoiceAttachments",
column: "TenantId");
migrationBuilder.CreateIndex(
name: "IX_InvoiceComments_CreatedById",
table: "InvoiceComments",
column: "CreatedById");
migrationBuilder.CreateIndex(
name: "IX_InvoiceComments_InvoiceId",
table: "InvoiceComments",
column: "InvoiceId");
migrationBuilder.CreateIndex(
name: "IX_InvoiceComments_TenantId",
table: "InvoiceComments",
column: "TenantId");
migrationBuilder.CreateIndex(
name: "IX_Invoices_CreatedById",
table: "Invoices",
column: "CreatedById");
migrationBuilder.CreateIndex(
name: "IX_Invoices_ProjectId",
table: "Invoices",
column: "ProjectId");
migrationBuilder.CreateIndex(
name: "IX_Invoices_TenantId",
table: "Invoices",
column: "TenantId");
migrationBuilder.CreateIndex(
name: "IX_Invoices_UpdatedById",
table: "Invoices",
column: "UpdatedById");
migrationBuilder.CreateIndex(
name: "IX_ReceivedInvoicePayments_CreatedById",
table: "ReceivedInvoicePayments",
column: "CreatedById");
migrationBuilder.CreateIndex(
name: "IX_ReceivedInvoicePayments_InvoiceId",
table: "ReceivedInvoicePayments",
column: "InvoiceId");
migrationBuilder.CreateIndex(
name: "IX_ReceivedInvoicePayments_TenantId",
table: "ReceivedInvoicePayments",
column: "TenantId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "InvoiceAttachments");
migrationBuilder.DropTable(
name: "InvoiceComments");
migrationBuilder.DropTable(
name: "ReceivedInvoicePayments");
migrationBuilder.DropTable(
name: "Invoices");
}
}
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,61 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Marco.Pms.DataAccess.Migrations
{
/// <inheritdoc />
public partial class Added_EInvoiceNumber_In_Invoice_Table : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
name: "Amount",
table: "Invoices",
newName: "TaxAmount");
migrationBuilder.AddColumn<double>(
name: "BasicAmount",
table: "Invoices",
type: "double",
nullable: false,
defaultValue: 0.0);
migrationBuilder.AddColumn<string>(
name: "EInvoiceNumber",
table: "Invoices",
type: "longtext",
nullable: true)
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.AddColumn<bool>(
name: "MarkAsCompleted",
table: "Invoices",
type: "tinyint(1)",
nullable: false,
defaultValue: false);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "BasicAmount",
table: "Invoices");
migrationBuilder.DropColumn(
name: "EInvoiceNumber",
table: "Invoices");
migrationBuilder.DropColumn(
name: "MarkAsCompleted",
table: "Invoices");
migrationBuilder.RenameColumn(
name: "TaxAmount",
table: "Invoices",
newName: "Amount");
}
}
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Marco.Pms.DataAccess.Migrations
{
/// <inheritdoc />
public partial class Added_Comment_In_ReceivedInvoicePayment_Table : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "Comment",
table: "ReceivedInvoicePayments",
type: "longtext",
nullable: false)
.Annotation("MySql:CharSet", "utf8mb4");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Comment",
table: "ReceivedInvoicePayments");
}
}
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,67 @@
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_PaymentAdjustmentHead_Master_Table : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "PaymentAdjustmentHeads",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
Name = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Description = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
IsActive = table.Column<bool>(type: "tinyint(1)", nullable: false),
TenantId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci")
},
constraints: table =>
{
table.PrimaryKey("PK_PaymentAdjustmentHeads", x => x.Id);
table.ForeignKey(
name: "FK_PaymentAdjustmentHeads_Tenants_TenantId",
column: x => x.TenantId,
principalTable: "Tenants",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.InsertData(
table: "PaymentAdjustmentHeads",
columns: new[] { "Id", "Description", "IsActive", "Name", "TenantId" },
values: new object[,]
{
{ new Guid("0d70cb2e-827e-44fc-90a5-c2c55ba51ba9"), "TDS, or Tax Deducted at Source, is a system under the Indian Income Tax Act where tax is deducted at the point of income generation—such as salary, interest, or rent—and remitted to the government to prevent tax evasion and ensure timely collection.", true, "Tax Deducted at Source (TDS)", new Guid("b3466e83-7e11-464c-b93a-daf047838b26") },
{ new Guid("3f09b19a-8d45-4cf2-be27-f4f09b38b9f7"), "Tax is a mandatory financial charge imposed by a government on individuals or entities to fund public services and government operations, without direct benefit to the taxpayer.", true, "Tax", new Guid("b3466e83-7e11-464c-b93a-daf047838b26") },
{ new Guid("50584332-1cb7-4359-9721-c8ea35040881"), "Utility fees are recurring charges for essential services such as electricity, water, gas, sewage, waste disposal, internet, and telecommunications, typically based on usage and necessary for operating a home or business.", true, "Utility fees", new Guid("b3466e83-7e11-464c-b93a-daf047838b26") },
{ new Guid("66c3c241-8b52-4327-a5ad-c1faf102583e"), "The base amount refers to the principal sum or original value used as a reference in financial calculations, excluding taxes, fees, or additional charges.", true, "Base Amount", new Guid("b3466e83-7e11-464c-b93a-daf047838b26") },
{ new Guid("95f35acd-d979-4177-91ea-fd03a00e49ff"), "Retention refers to a company's ability to keep customers, employees, or profits over time, commonly measured as a percentage and critical for long-term business sustainability and growth.", true, "Retention", new Guid("b3466e83-7e11-464c-b93a-daf047838b26") },
{ new Guid("dbdc047f-a2d2-4db0-b0e6-b9d9f923a0f1"), "An advance payment is a sum paid before receiving goods or services, often to secure a transaction or cover initial costs.", true, "Advance payment", new Guid("b3466e83-7e11-464c-b93a-daf047838b26") },
{ new Guid("ec5e6a5f-ce62-44e5-8911-8426bbb4dde8"), "A penalty in the context of taxation is a financial sanction imposed by the government on individuals or entities for non-compliance with tax laws, such as late filing, underreporting income, or failure to pay taxes, and is typically calculated as a percentage of the tax due or a fixed amount.", true, "Penalty", new Guid("b3466e83-7e11-464c-b93a-daf047838b26") }
});
migrationBuilder.CreateIndex(
name: "IX_PaymentAdjustmentHeads_TenantId",
table: "PaymentAdjustmentHeads",
column: "TenantId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "PaymentAdjustmentHeads");
}
}
}

View File

@ -0,0 +1,51 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Marco.Pms.DataAccess.Migrations
{
/// <inheritdoc />
public partial class Added_PaymentAdjustmentHead_ForignKey_In_ReceivedInvoicePayment_Table : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<Guid>(
name: "PaymentAdjustmentHeadId",
table: "ReceivedInvoicePayments",
type: "char(36)",
nullable: false,
defaultValue: new Guid("66c3c241-8b52-4327-a5ad-c1faf102583e"),
collation: "ascii_general_ci");
migrationBuilder.CreateIndex(
name: "IX_ReceivedInvoicePayments_PaymentAdjustmentHeadId",
table: "ReceivedInvoicePayments",
column: "PaymentAdjustmentHeadId");
migrationBuilder.AddForeignKey(
name: "FK_ReceivedInvoicePayments_PaymentAdjustmentHeads_PaymentAdjust~",
table: "ReceivedInvoicePayments",
column: "PaymentAdjustmentHeadId",
principalTable: "PaymentAdjustmentHeads",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_ReceivedInvoicePayments_PaymentAdjustmentHeads_PaymentAdjust~",
table: "ReceivedInvoicePayments");
migrationBuilder.DropIndex(
name: "IX_ReceivedInvoicePayments_PaymentAdjustmentHeadId",
table: "ReceivedInvoicePayments");
migrationBuilder.DropColumn(
name: "PaymentAdjustmentHeadId",
table: "ReceivedInvoicePayments");
}
}
}

View File

@ -369,6 +369,273 @@ namespace Marco.Pms.DataAccess.Migrations
b.ToTable("RefreshTokens"); b.ToTable("RefreshTokens");
}); });
modelBuilder.Entity("Marco.Pms.Model.Collection.Invoice", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<double>("BasicAmount")
.HasColumnType("double");
b.Property<DateTime>("ClientSubmitedDate")
.HasColumnType("datetime(6)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<Guid>("CreatedById")
.HasColumnType("char(36)");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("EInvoiceNumber")
.HasColumnType("longtext");
b.Property<DateTime>("ExceptedPaymentDate")
.HasColumnType("datetime(6)");
b.Property<DateTime>("InvoiceDate")
.HasColumnType("datetime(6)");
b.Property<string>("InvoiceNumber")
.IsRequired()
.HasColumnType("longtext");
b.Property<bool>("IsActive")
.HasColumnType("tinyint(1)");
b.Property<bool>("MarkAsCompleted")
.HasColumnType("tinyint(1)");
b.Property<Guid>("ProjectId")
.HasColumnType("char(36)");
b.Property<double>("TaxAmount")
.HasColumnType("double");
b.Property<Guid>("TenantId")
.HasColumnType("char(36)");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("longtext");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime(6)");
b.Property<Guid?>("UpdatedById")
.HasColumnType("char(36)");
b.HasKey("Id");
b.HasIndex("CreatedById");
b.HasIndex("ProjectId");
b.HasIndex("TenantId");
b.HasIndex("UpdatedById");
b.ToTable("Invoices");
});
modelBuilder.Entity("Marco.Pms.Model.Collection.InvoiceAttachment", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<Guid>("DocumentId")
.HasColumnType("char(36)");
b.Property<Guid>("InvoiceId")
.HasColumnType("char(36)");
b.Property<Guid>("TenantId")
.HasColumnType("char(36)");
b.HasKey("Id");
b.HasIndex("DocumentId");
b.HasIndex("InvoiceId");
b.HasIndex("TenantId");
b.ToTable("InvoiceAttachments");
});
modelBuilder.Entity("Marco.Pms.Model.Collection.InvoiceComment", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<string>("Comment")
.IsRequired()
.HasColumnType("longtext");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<Guid>("CreatedById")
.HasColumnType("char(36)");
b.Property<Guid>("InvoiceId")
.HasColumnType("char(36)");
b.Property<Guid>("TenantId")
.HasColumnType("char(36)");
b.HasKey("Id");
b.HasIndex("CreatedById");
b.HasIndex("InvoiceId");
b.HasIndex("TenantId");
b.ToTable("InvoiceComments");
});
modelBuilder.Entity("Marco.Pms.Model.Collection.PaymentAdjustmentHead", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<string>("Description")
.HasColumnType("longtext");
b.Property<bool>("IsActive")
.HasColumnType("tinyint(1)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.Property<Guid>("TenantId")
.HasColumnType("char(36)");
b.HasKey("Id");
b.HasIndex("TenantId");
b.ToTable("PaymentAdjustmentHeads");
b.HasData(
new
{
Id = new Guid("dbdc047f-a2d2-4db0-b0e6-b9d9f923a0f1"),
Description = "An advance payment is a sum paid before receiving goods or services, often to secure a transaction or cover initial costs.",
IsActive = true,
Name = "Advance payment",
TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26")
},
new
{
Id = new Guid("66c3c241-8b52-4327-a5ad-c1faf102583e"),
Description = "The base amount refers to the principal sum or original value used as a reference in financial calculations, excluding taxes, fees, or additional charges.",
IsActive = true,
Name = "Base Amount",
TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26")
},
new
{
Id = new Guid("0d70cb2e-827e-44fc-90a5-c2c55ba51ba9"),
Description = "TDS, or Tax Deducted at Source, is a system under the Indian Income Tax Act where tax is deducted at the point of income generation—such as salary, interest, or rent—and remitted to the government to prevent tax evasion and ensure timely collection.",
IsActive = true,
Name = "Tax Deducted at Source (TDS)",
TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26")
},
new
{
Id = new Guid("95f35acd-d979-4177-91ea-fd03a00e49ff"),
Description = "Retention refers to a company's ability to keep customers, employees, or profits over time, commonly measured as a percentage and critical for long-term business sustainability and growth.",
IsActive = true,
Name = "Retention",
TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26")
},
new
{
Id = new Guid("3f09b19a-8d45-4cf2-be27-f4f09b38b9f7"),
Description = "Tax is a mandatory financial charge imposed by a government on individuals or entities to fund public services and government operations, without direct benefit to the taxpayer.",
IsActive = true,
Name = "Tax",
TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26")
},
new
{
Id = new Guid("ec5e6a5f-ce62-44e5-8911-8426bbb4dde8"),
Description = "A penalty in the context of taxation is a financial sanction imposed by the government on individuals or entities for non-compliance with tax laws, such as late filing, underreporting income, or failure to pay taxes, and is typically calculated as a percentage of the tax due or a fixed amount.",
IsActive = true,
Name = "Penalty",
TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26")
},
new
{
Id = new Guid("50584332-1cb7-4359-9721-c8ea35040881"),
Description = "Utility fees are recurring charges for essential services such as electricity, water, gas, sewage, waste disposal, internet, and telecommunications, typically based on usage and necessary for operating a home or business.",
IsActive = true,
Name = "Utility fees",
TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26")
});
});
modelBuilder.Entity("Marco.Pms.Model.Collection.ReceivedInvoicePayment", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<double>("Amount")
.HasColumnType("double");
b.Property<string>("Comment")
.IsRequired()
.HasColumnType("longtext");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<Guid>("CreatedById")
.HasColumnType("char(36)");
b.Property<Guid>("InvoiceId")
.HasColumnType("char(36)");
b.Property<bool>("IsActive")
.HasColumnType("tinyint(1)");
b.Property<Guid>("PaymentAdjustmentHeadId")
.HasColumnType("char(36)");
b.Property<DateTime>("PaymentReceivedDate")
.HasColumnType("datetime(6)");
b.Property<Guid>("TenantId")
.HasColumnType("char(36)");
b.Property<string>("TransactionId")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.HasIndex("CreatedById");
b.HasIndex("InvoiceId");
b.HasIndex("PaymentAdjustmentHeadId");
b.HasIndex("TenantId");
b.ToTable("ReceivedInvoicePayments");
});
modelBuilder.Entity("Marco.Pms.Model.Directory.Bucket", b => modelBuilder.Entity("Marco.Pms.Model.Directory.Bucket", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
@ -4794,6 +5061,139 @@ namespace Marco.Pms.DataAccess.Migrations
b.Navigation("User"); b.Navigation("User");
}); });
modelBuilder.Entity("Marco.Pms.Model.Collection.Invoice", b =>
{
b.HasOne("Marco.Pms.Model.Employees.Employee", "CreatedBy")
.WithMany()
.HasForeignKey("CreatedById")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Marco.Pms.Model.Projects.Project", "Project")
.WithMany()
.HasForeignKey("ProjectId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Marco.Pms.Model.TenantModels.Tenant", "Tenant")
.WithMany()
.HasForeignKey("TenantId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Marco.Pms.Model.Employees.Employee", "UpdatedBy")
.WithMany()
.HasForeignKey("UpdatedById");
b.Navigation("CreatedBy");
b.Navigation("Project");
b.Navigation("Tenant");
b.Navigation("UpdatedBy");
});
modelBuilder.Entity("Marco.Pms.Model.Collection.InvoiceAttachment", b =>
{
b.HasOne("Marco.Pms.Model.DocumentManager.Document", "Document")
.WithMany()
.HasForeignKey("DocumentId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Marco.Pms.Model.Collection.Invoice", "Invoice")
.WithMany()
.HasForeignKey("InvoiceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Marco.Pms.Model.TenantModels.Tenant", "Tenant")
.WithMany()
.HasForeignKey("TenantId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Document");
b.Navigation("Invoice");
b.Navigation("Tenant");
});
modelBuilder.Entity("Marco.Pms.Model.Collection.InvoiceComment", b =>
{
b.HasOne("Marco.Pms.Model.Employees.Employee", "CreatedBy")
.WithMany()
.HasForeignKey("CreatedById")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Marco.Pms.Model.Collection.Invoice", "Invoice")
.WithMany()
.HasForeignKey("InvoiceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Marco.Pms.Model.TenantModels.Tenant", "Tenant")
.WithMany()
.HasForeignKey("TenantId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("CreatedBy");
b.Navigation("Invoice");
b.Navigation("Tenant");
});
modelBuilder.Entity("Marco.Pms.Model.Collection.PaymentAdjustmentHead", b =>
{
b.HasOne("Marco.Pms.Model.TenantModels.Tenant", "Tenant")
.WithMany()
.HasForeignKey("TenantId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Tenant");
});
modelBuilder.Entity("Marco.Pms.Model.Collection.ReceivedInvoicePayment", b =>
{
b.HasOne("Marco.Pms.Model.Employees.Employee", "CreatedBy")
.WithMany()
.HasForeignKey("CreatedById")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Marco.Pms.Model.Collection.Invoice", "Invoice")
.WithMany()
.HasForeignKey("InvoiceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Marco.Pms.Model.Collection.PaymentAdjustmentHead", "PaymentAdjustmentHead")
.WithMany()
.HasForeignKey("PaymentAdjustmentHeadId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Marco.Pms.Model.TenantModels.Tenant", "Tenant")
.WithMany()
.HasForeignKey("TenantId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("CreatedBy");
b.Navigation("Invoice");
b.Navigation("PaymentAdjustmentHead");
b.Navigation("Tenant");
});
modelBuilder.Entity("Marco.Pms.Model.Directory.Bucket", b => modelBuilder.Entity("Marco.Pms.Model.Directory.Bucket", b =>
{ {
b.HasOne("Marco.Pms.Model.Employees.Employee", "CreatedBy") b.HasOne("Marco.Pms.Model.Employees.Employee", "CreatedBy")

View File

@ -0,0 +1,41 @@
using Marco.Pms.Model.Employees;
using Marco.Pms.Model.Projects;
using Marco.Pms.Model.Utilities;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using System.ComponentModel.DataAnnotations.Schema;
namespace Marco.Pms.Model.Collection
{
public class Invoice : TenantRelation
{
public Guid Id { get; set; }
public string Title { get; set; } = default!;
public string Description { get; set; } = default!;
public string InvoiceNumber { get; set; } = default!;
public string? EInvoiceNumber { get; set; }
public Guid ProjectId { get; set; }
[ValidateNever]
[ForeignKey("ProjectId")]
public Project? Project { get; set; }
public DateTime InvoiceDate { get; set; }
public DateTime ClientSubmitedDate { get; set; }
public DateTime ExceptedPaymentDate { get; set; }
public double BasicAmount { get; set; }
public double TaxAmount { get; set; }
public bool IsActive { get; set; } = true;
public bool MarkAsCompleted { get; set; } = true;
public DateTime CreatedAt { get; set; }
public Guid CreatedById { get; set; }
[ValidateNever]
[ForeignKey("CreatedById")]
public Employee? CreatedBy { get; set; }
public DateTime? UpdatedAt { get; set; }
public Guid? UpdatedById { get; set; }
[ValidateNever]
[ForeignKey("UpdatedById")]
public Employee? UpdatedBy { get; set; }
}
}

View File

@ -0,0 +1,22 @@
using Marco.Pms.Model.DocumentManager;
using Marco.Pms.Model.Utilities;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using System.ComponentModel.DataAnnotations.Schema;
namespace Marco.Pms.Model.Collection
{
public class InvoiceAttachment : TenantRelation
{
public Guid Id { get; set; }
public Guid InvoiceId { get; set; }
[ValidateNever]
[ForeignKey("InvoiceId")]
public Invoice? Invoice { get; set; }
public Guid DocumentId { get; set; }
[ValidateNever]
[ForeignKey("DocumentId")]
public Document? Document { get; set; }
}
}

View File

@ -0,0 +1,24 @@
using Marco.Pms.Model.Employees;
using Marco.Pms.Model.Utilities;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using System.ComponentModel.DataAnnotations.Schema;
namespace Marco.Pms.Model.Collection
{
public class InvoiceComment : TenantRelation
{
public Guid Id { get; set; }
public string Comment { get; set; } = default!;
public DateTime CreatedAt { get; set; }
public Guid CreatedById { get; set; }
[ValidateNever]
[ForeignKey("CreatedById")]
public Employee? CreatedBy { get; set; }
public Guid InvoiceId { get; set; }
[ValidateNever]
[ForeignKey("InvoiceId")]
public Invoice? Invoice { get; set; }
}
}

View File

@ -0,0 +1,12 @@
using Marco.Pms.Model.Utilities;
namespace Marco.Pms.Model.Collection
{
public class PaymentAdjustmentHead : TenantRelation
{
public Guid Id { get; set; }
public string Name { get; set; } = default!;
public string? Description { get; set; }
public bool IsActive { get; set; } = true;
}
}

View File

@ -0,0 +1,33 @@
using Marco.Pms.Model.Employees;
using Marco.Pms.Model.Utilities;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using System.ComponentModel.DataAnnotations.Schema;
namespace Marco.Pms.Model.Collection
{
public class ReceivedInvoicePayment : TenantRelation
{
public Guid Id { get; set; }
public Guid InvoiceId { get; set; }
[ValidateNever]
[ForeignKey("InvoiceId")]
public Invoice? Invoice { get; set; }
public DateTime PaymentReceivedDate { get; set; }
public string TransactionId { get; set; } = default!;
public double Amount { get; set; }
public string Comment { get; set; } = default!;
public bool IsActive { get; set; } = true;
public Guid PaymentAdjustmentHeadId { get; set; }
[ValidateNever]
[ForeignKey("PaymentAdjustmentHeadId")]
public PaymentAdjustmentHead? PaymentAdjustmentHead { get; set; }
public DateTime CreatedAt { get; set; }
public Guid CreatedById { get; set; }
[ValidateNever]
[ForeignKey("CreatedById")]
public Employee? CreatedBy { get; set; }
}
}

View File

@ -0,0 +1,8 @@
namespace Marco.Pms.Model.Dtos.Collection
{
public class InvoiceCommentDto
{
public required string Comment { get; set; }
public required Guid InvoiceId { get; set; }
}
}

View File

@ -0,0 +1,20 @@
using Marco.Pms.Model.Utilities;
namespace Marco.Pms.Model.Dtos.Collection
{
public class InvoiceDto
{
public Guid? Id { get; set; }
public required string Title { get; set; }
public string? Description { get; set; }
public required string InvoiceNumber { get; set; }
public string? EInvoiceNumber { get; set; }
public required Guid ProjectId { get; set; }
public required DateTime InvoiceDate { get; set; }
public required DateTime ClientSubmitedDate { get; set; }
public required DateTime ExceptedPaymentDate { get; set; }
public double BasicAmount { get; set; }
public double TaxAmount { get; set; }
public List<FileUploadModel>? Attachments { get; set; }
}
}

View File

@ -0,0 +1,9 @@
namespace Marco.Pms.Model.Dtos.Collection
{
public class PaymentAdjustmentHeadDto
{
public Guid? Id { get; set; }
public required string Name { get; set; }
public string? Description { get; set; }
}
}

View File

@ -0,0 +1,13 @@
namespace Marco.Pms.Model.Dtos.Collection
{
public class ReceivedInvoicePaymentDto
{
public Guid? Id { get; set; }
public required Guid InvoiceId { get; set; }
public required DateTime PaymentReceivedDate { get; set; }
public required string TransactionId { get; set; }
public required Guid PaymentAdjustmentHeadId { get; set; }
public required double Amount { get; set; }
public required string Comment { get; set; }
}
}

View File

@ -0,0 +1,16 @@
using Marco.Pms.Model.ViewModels.Activities;
namespace Marco.Pms.Model.ViewModels.Collection
{
public class InvoiceAttachmentVM
{
public Guid Id { get; set; }
public Guid InvoiceId { get; set; }
public Guid DocumentId { get; set; }
public string? FileName { get; set; }
public string? ContentType { get; set; }
public string? PreSignedUrl { get; set; }
public BasicEmployeeVM? UploadedBy { get; set; }
}
}

View File

@ -0,0 +1,13 @@
using Marco.Pms.Model.ViewModels.Activities;
namespace Marco.Pms.Model.ViewModels.Collection
{
public class InvoiceCommentVM
{
public Guid Id { get; set; }
public string Comment { get; set; } = default!;
public DateTime CreatedAt { get; set; }
public BasicEmployeeVM? CreatedBy { get; set; }
public Guid InvoiceId { get; set; }
}
}

View File

@ -0,0 +1,31 @@
using Marco.Pms.Model.ViewModels.Activities;
using Marco.Pms.Model.ViewModels.Projects;
namespace Marco.Pms.Model.ViewModels.Collection
{
public class InvoiceDetailsVM
{
public Guid Id { get; set; }
public string Title { get; set; } = default!;
public string Description { get; set; } = default!;
public string InvoiceNumber { get; set; } = default!;
public string? EInvoiceNumber { get; set; }
public BasicProjectVM? Project { get; set; }
public DateTime InvoiceDate { get; set; }
public DateTime ClientSubmitedDate { get; set; }
public DateTime ExceptedPaymentDate { get; set; }
public double BasicAmount { get; set; }
public double TaxAmount { get; set; }
public double BalanceAmount { get; set; }
public bool IsActive { get; set; } = true;
public bool MarkAsCompleted { get; set; } = true;
public DateTime CreatedAt { get; set; }
public BasicEmployeeVM? CreatedBy { get; set; }
public DateTime? UpdatedAt { get; set; }
public BasicEmployeeVM? UpdatedBy { get; set; }
public List<InvoiceAttachmentVM>? Attachments { get; set; }
public List<ReceivedInvoicePaymentVM>? ReceivedInvoicePayments { get; set; }
public List<InvoiceCommentVM>? Comments { get; set; }
}
}

View File

@ -0,0 +1,27 @@
using Marco.Pms.Model.ViewModels.Activities;
using Marco.Pms.Model.ViewModels.Projects;
namespace Marco.Pms.Model.ViewModels.Collection
{
public class InvoiceListVM
{
public Guid Id { get; set; }
public string Title { get; set; } = default!;
public string Description { get; set; } = default!;
public string InvoiceNumber { get; set; } = default!;
public string? EInvoiceNumber { get; set; }
public BasicProjectVM? Project { get; set; }
public DateTime InvoiceDate { get; set; }
public DateTime ClientSubmitedDate { get; set; }
public DateTime ExceptedPaymentDate { get; set; }
public double BasicAmount { get; set; }
public double TaxAmount { get; set; }
public double BalanceAmount { get; set; }
public bool IsActive { get; set; }
public bool MarkAsCompleted { get; set; }
public DateTime CreatedAt { get; set; }
public BasicEmployeeVM? CreatedBy { get; set; }
public DateTime? UpdatedAt { get; set; }
public BasicEmployeeVM? UpdatedBy { get; set; }
}
}

View File

@ -0,0 +1,10 @@
namespace Marco.Pms.Model.ViewModels.Collection
{
public class PaymentAdjustmentHeadVM
{
public Guid Id { get; set; }
public string? Name { get; set; }
public string? Description { get; set; }
public bool IsActive { get; set; } = true;
}
}

View File

@ -0,0 +1,18 @@
using Marco.Pms.Model.ViewModels.Activities;
namespace Marco.Pms.Model.ViewModels.Collection
{
public class ReceivedInvoicePaymentVM
{
public Guid Id { get; set; }
public Guid InvoiceId { get; set; }
public DateTime PaymentReceivedDate { get; set; }
public PaymentAdjustmentHeadVM? PaymentAdjustmentHead { get; set; }
public string TransactionId { get; set; } = default!;
public double Amount { get; set; }
public string? Comment { get; set; }
public bool IsActive { get; set; } = true;
public DateTime CreatedAt { get; set; }
public BasicEmployeeVM? CreatedBy { get; set; }
}
}

View File

@ -577,7 +577,6 @@ namespace Marco.Pms.Services.Controllers
{ {
new MasterMenuVM { Id = 3, Name = "Work Category" }, new MasterMenuVM { Id = 3, Name = "Work Category" },
new MasterMenuVM { Id = 8, Name = "Services" } new MasterMenuVM { Id = 8, Name = "Services" }
//new MasterMenuVM { Id = 10, Name = "Payment Mode" }
} }
}, },
{ {
@ -591,7 +590,8 @@ namespace Marco.Pms.Services.Controllers
ExpenseManagement, new List<MasterMenuVM> ExpenseManagement, new List<MasterMenuVM>
{ {
new MasterMenuVM { Id = 6, Name = "Expense Type" }, new MasterMenuVM { Id = 6, Name = "Expense Type" },
new MasterMenuVM { Id = 7, Name = "Payment Mode" } new MasterMenuVM { Id = 7, Name = "Payment Mode" },
new MasterMenuVM { Id = 10, Name = "Payment Adjustment Head" }
} }
} }
}; };
@ -665,6 +665,46 @@ namespace Marco.Pms.Services.Controllers
Available = true Available = true
}; };
response.Add(menuVM); response.Add(menuVM);
if (item.Submenu?.Any() == true)
{
var allowedSubmenus = new List<SubMenuItem>();
foreach (var subItem in item.Submenu)
{
if (!subItem.PermissionIds.Any())
{
MenuSectionApplicationVM subMenuVM = new MenuSectionApplicationVM
{
Id = subItem.Id,
Name = subItem.Text,
Available = true
};
response.Add(subMenuVM);
continue;
}
var subMenuPermissionIds = subItem.PermissionIds
.Select(Guid.Parse)
.ToList();
bool isSubItemAllowed = await _permissions.HasPermissionAny(subMenuPermissionIds, employeeId);
if (isSubItemAllowed)
{
MenuSectionApplicationVM subMenuVM = new MenuSectionApplicationVM
{
Id = subItem.Id,
Name = subItem.Text,
Available = true
};
response.Add(subMenuVM);
}
}
// Replace with filtered submenus
item.Submenu = allowedSubmenus;
}
} }
else else
{ {

View File

@ -321,7 +321,6 @@ namespace MarcoBMS.Services.Controllers
return Ok(ApiResponse<object>.SuccessResponse(result, System.String.Format("{0} Attendance records fetched successfully", result.Count), 200)); return Ok(ApiResponse<object>.SuccessResponse(result, System.String.Format("{0} Attendance records fetched successfully", result.Count), 200));
} }
[HttpGet("project/team")]
/// <summary> /// <summary>
/// Retrieves employee attendance records for a specified project and date. /// Retrieves employee attendance records for a specified project and date.
/// The result is filtered based on the logged-in employee's permissions (Team or Self). /// The result is filtered based on the logged-in employee's permissions (Team or Self).
@ -331,6 +330,8 @@ namespace MarcoBMS.Services.Controllers
/// <param name="includeInactive">Optional. Includes inactive employees in the team list if true.</param> /// <param name="includeInactive">Optional. Includes inactive employees in the team list if true.</param>
/// <param name="date">Optional. The date for which to fetch attendance, in "yyyy-MM-dd" format. Defaults to the current UTC date.</param> /// <param name="date">Optional. The date for which to fetch attendance, in "yyyy-MM-dd" format. Defaults to the current UTC date.</param>
/// <returns>An IActionResult containing a list of employee attendance records or an error response.</returns> /// <returns>An IActionResult containing a list of employee attendance records or an error response.</returns>
[HttpGet("project/team")]
public async Task<IActionResult> EmployeeAttendanceByProjectAsync([FromQuery] Guid projectId, [FromQuery] Guid? organizationId, [FromQuery] bool includeInactive, [FromQuery] string? date = null) public async Task<IActionResult> EmployeeAttendanceByProjectAsync([FromQuery] Guid projectId, [FromQuery] Guid? organizationId, [FromQuery] bool includeInactive, [FromQuery] string? date = null)
{ {
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
@ -390,7 +391,6 @@ namespace MarcoBMS.Services.Controllers
} }
} }
[HttpGet("regularize")] [HttpGet("regularize")]
public async Task<IActionResult> GetRequestRegularizeAttendance([FromQuery] Guid projectId, [FromQuery] Guid? organizationId, [FromQuery] bool IncludeInActive) public async Task<IActionResult> GetRequestRegularizeAttendance([FromQuery] Guid projectId, [FromQuery] Guid? organizationId, [FromQuery] bool IncludeInActive)
{ {
@ -456,8 +456,7 @@ namespace MarcoBMS.Services.Controllers
return Ok(ApiResponse<object>.SuccessResponse(result, System.String.Format("{0} Attendance records fetched successfully", result.Count), 200)); return Ok(ApiResponse<object>.SuccessResponse(result, System.String.Format("{0} Attendance records fetched successfully", result.Count), 200));
} }
[HttpPost] [HttpPost("record")]
[Route("record")]
public async Task<IActionResult> RecordAttendance([FromBody] RecordAttendanceDot recordAttendanceDot) public async Task<IActionResult> RecordAttendance([FromBody] RecordAttendanceDot recordAttendanceDot)
{ {
if (!ModelState.IsValid) if (!ModelState.IsValid)
@ -612,7 +611,8 @@ namespace MarcoBMS.Services.Controllers
LastName = employee.LastName, LastName = employee.LastName,
Id = attendance.Id, Id = attendance.Id,
Activity = attendance.Activity, Activity = attendance.Activity,
JobRoleName = employee.JobRole.Name JobRoleName = employee.JobRole.Name,
OrganizationName = employee.Organization?.Name
}; };
var sendActivity = 0; var sendActivity = 0;
if (recordAttendanceDot.Id == Guid.Empty) if (recordAttendanceDot.Id == Guid.Empty)

View File

@ -0,0 +1,903 @@
using AutoMapper;
using Marco.Pms.DataAccess.Data;
using Marco.Pms.Helpers.Utility;
using Marco.Pms.Model.Collection;
using Marco.Pms.Model.DocumentManager;
using Marco.Pms.Model.Dtos.Collection;
using Marco.Pms.Model.MongoDBModels.Utility;
using Marco.Pms.Model.Utilities;
using Marco.Pms.Model.ViewModels.Activities;
using Marco.Pms.Model.ViewModels.Collection;
using Marco.Pms.Model.ViewModels.Projects;
using Marco.Pms.Services.Service;
using MarcoBMS.Services.Helpers;
using MarcoBMS.Services.Service;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MongoDB.Driver;
namespace Marco.Pms.Services.Controllers
{
[Route("api/[controller]")]
[ApiController]
[Authorize]
public class CollectionController : ControllerBase
{
private readonly IDbContextFactory<ApplicationDbContext> _dbContextFactory;
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly UserHelper _userHelper;
private readonly S3UploadService _s3Service;
private readonly IMapper _mapper;
private readonly ILoggingService _logger;
private readonly Guid tenantId;
public CollectionController(IDbContextFactory<ApplicationDbContext> dbContextFactory,
IServiceScopeFactory serviceScopeFactory,
S3UploadService s3Service,
UserHelper userhelper,
ILoggingService logger,
IMapper mapper)
{
_dbContextFactory = dbContextFactory;
_serviceScopeFactory = serviceScopeFactory;
_userHelper = userhelper;
_s3Service = s3Service;
_mapper = mapper;
_logger = logger;
tenantId = userhelper.GetTenantId();
}
#region =================================================================== Get Functions ===================================================================
[HttpGet("invoice/list")]
public async Task<IActionResult> GetInvoiceListAsync([FromQuery] Guid? projectId, [FromQuery] string? searchString, [FromQuery] DateTime? fromDate, [FromQuery] DateTime? toDate, [FromQuery] int pageSize = 20, [FromQuery] int pageNumber = 1
, [FromQuery] bool isActive = true, [FromQuery] bool isPending = false)
{
_logger.LogInfo(
"Fetching invoice list: Page {PageNumber}, Size {PageSize}, Active={IsActive}, PendingOnly={IsPending}, Search='{SearchString}', From={From}, To={To}",
pageNumber, pageSize, isActive, isPending, searchString ?? "", fromDate?.Date ?? DateTime.MinValue, toDate?.Date ?? DateTime.MaxValue);
await using var _context = await _dbContextFactory.CreateDbContextAsync();
// Build base query with required includes and no tracking
var invoicesQuery = _context.Invoices
.Include(i => i.Project)
.Include(i => i.CreatedBy).ThenInclude(e => e!.JobRole)
.Include(i => i.UpdatedBy).ThenInclude(e => e!.JobRole)
.Where(i => i.IsActive == isActive && i.TenantId == tenantId)
.AsNoTracking(); // Disable change tracking for read-only query
// Apply date filter
if (fromDate.HasValue && toDate.HasValue)
{
var fromDateUtc = fromDate.Value.Date;
var toDateUtc = toDate.Value.Date.AddDays(1).AddTicks(-1); // End of day
invoicesQuery = invoicesQuery.Where(i => i.InvoiceDate >= fromDateUtc && i.InvoiceDate <= toDateUtc);
_logger.LogDebug("Applied date filter: {From} to {To}", fromDateUtc, toDateUtc);
}
// Apply search filter
if (!string.IsNullOrWhiteSpace(searchString))
{
invoicesQuery = invoicesQuery.Where(i => i.Title.Contains(searchString) || i.InvoiceNumber.Contains(searchString));
_logger.LogDebug("Applied search filter with term: {SearchString}", searchString);
}
// Apply project filter
if (projectId.HasValue)
{
invoicesQuery = invoicesQuery.Where(i => i.ProjectId == projectId.Value);
_logger.LogDebug("Applied project filter with term: {ProjectId}", projectId);
}
// Get total count before pagination
var totalEntites = await invoicesQuery.CountAsync();
_logger.LogDebug("Total matching invoices: {TotalCount}", totalEntites);
// Apply sorting and pagination
var invoices = await invoicesQuery
.OrderByDescending(i => i.InvoiceDate)
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
if (!invoices.Any())
{
_logger.LogInfo("No invoices found for the given criteria.");
var emptyResponse = new
{
CurrentPage = pageNumber,
TotalPages = 0,
TotalEntites = 0,
Data = new List<InvoiceListVM>()
};
return Ok(ApiResponse<object>.SuccessResponse(emptyResponse, "No invoices found"));
}
// Fetch all related payment data in a single query
var invoiceIds = invoices.Select(i => i.Id).ToList();
var paymentGroups = await _context.ReceivedInvoicePayments
.AsNoTracking()
.Where(rip => invoiceIds.Contains(rip.InvoiceId) && rip.TenantId == tenantId)
.GroupBy(rip => rip.InvoiceId)
.Select(g => new
{
InvoiceId = g.Key,
PaidAmount = g.Sum(rip => rip.Amount)
})
.ToDictionaryAsync(x => x.InvoiceId, x => x.PaidAmount);
_logger.LogDebug("Fetched payment data for {Count} invoices", paymentGroups.Count);
// Map and calculate balance in memory
var results = new List<InvoiceListVM>();
foreach (var invoice in invoices)
{
var totalAmount = invoice.BasicAmount + invoice.TaxAmount;
var paidAmount = paymentGroups.GetValueOrDefault(invoice.Id, 0);
var balanceAmount = totalAmount - paidAmount;
// Skip if filtering for pending invoices and balance is zero
if (isPending && balanceAmount <= 0)
continue;
var result = _mapper.Map<InvoiceListVM>(invoice);
result.BalanceAmount = balanceAmount;
results.Add(result);
}
var totalPages = (int)Math.Ceiling((double)totalEntites / pageSize);
var response = new
{
CurrentPage = pageNumber,
TotalPages = totalPages,
TotalEntites = totalEntites,
Data = results
};
_logger.LogInfo("Successfully returned {ResultCount} invoices out of {TotalCount} total", results.Count, totalEntites);
return Ok(ApiResponse<object>.SuccessResponse(response, $"{results.Count} invoices fetched successfully"));
}
/// <summary>
/// Retrieves complete details of a specific invoice including associated comments, attachments, and payments.
/// </summary>
/// <param name="id">The unique identifier of the invoice.</param>
/// <returns>Returns invoice details with associated data or a NotFound/BadRequest response.</returns>
[HttpGet("invoice/details/{id}")]
public async Task<IActionResult> GetInvoiceDetailsAsync(Guid id)
{
_logger.LogInfo("Fetching details for InvoiceId: {InvoiceId}, TenantId: {TenantId}", id, tenantId);
await using var context = await _dbContextFactory.CreateDbContextAsync();
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
// Retrieve primary invoice details with related entities (project, created/updated by + roles)
var invoice = await context.Invoices
.Include(i => i.Project)
.Include(i => i.CreatedBy).ThenInclude(e => e!.JobRole)
.Include(i => i.UpdatedBy).ThenInclude(e => e!.JobRole)
.AsNoTracking()
.FirstOrDefaultAsync(i => i.Id == id && i.TenantId == tenantId);
if (invoice == null)
{
_logger.LogWarning("Invoice not found. InvoiceId: {InvoiceId}, TenantId: {TenantId}", id, tenantId);
return NotFound(ApiResponse<object>.ErrorResponse("Invoice not found", "The specified invoice does not exist.", 404));
}
_logger.LogInfo("Invoice {InvoiceId} found. Fetching related data...", id);
// Parallelize loading of child collections using independent DbContext instances — efficient and thread-safe
var commentsTask = LoadInvoiceCommentsAsync(id, tenantId);
var attachmentsTask = LoadInvoiceAttachmentsAsync(id, tenantId);
var paymentsTask = LoadReceivedInvoicePaymentsAsync(id, tenantId);
await Task.WhenAll(commentsTask, attachmentsTask, paymentsTask);
var comments = commentsTask.Result;
var attachments = attachmentsTask.Result;
var receivedInvoicePayments = paymentsTask.Result;
// Map invoice to response view model
var response = _mapper.Map<InvoiceDetailsVM>(invoice);
// Populate related data
if (comments.Any())
response.Comments = _mapper.Map<List<InvoiceCommentVM>>(comments);
if (attachments.Any())
{
response.Attachments = attachments.Select(a =>
{
var result = _mapper.Map<InvoiceAttachmentVM>(a);
result.PreSignedUrl = _s3Service.GeneratePreSignedUrl(a.Document!.S3Key);
result.UploadedBy = _mapper.Map<BasicEmployeeVM>(a.Document.UploadedBy);
result.FileName = a.Document.FileName;
result.ContentType = a.Document.ContentType;
return result;
}).ToList();
}
if (receivedInvoicePayments.Any())
response.ReceivedInvoicePayments = _mapper.Map<List<ReceivedInvoicePaymentVM>>(receivedInvoicePayments);
// Compute total paid and balance amounts
double totalPaidAmount = receivedInvoicePayments.Sum(rip => rip.Amount);
double totalAmount = invoice.BasicAmount + invoice.TaxAmount;
response.BalanceAmount = totalAmount - totalPaidAmount;
_logger.LogInfo("Invoice {InvoiceId} details fetched successfully: Total = {TotalAmount}, Paid = {PaidAmount}, Balance = {BalanceAmount}",
id, totalAmount, totalPaidAmount, response.BalanceAmount);
return Ok(ApiResponse<object>.SuccessResponse(response, "Invoice details fetched successfully", 200));
}
#endregion
#region =================================================================== Post Functions ===================================================================
[HttpPost("invoice/create")]
public async Task<IActionResult> CreateInvoiceAsync([FromBody] InvoiceDto model)
{
await using var _context = await _dbContextFactory.CreateDbContextAsync();
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
_logger.LogInfo("Starting invoice creation for ProjectId: {ProjectId} by EmployeeId: {EmployeeId}",
model.ProjectId, loggedInEmployee.Id);
if (model.InvoiceNumber.Length > 17)
{
_logger.LogWarning("Invoice Number {InvoiceNumber} is greater than 17 charater",
model.InvoiceNumber);
return BadRequest(ApiResponse<object>.ErrorResponse(
"Invoice Number cannot be greater than 17 charater",
"Invoice Number is greater than 17 charater", 400));
}
// Validate date sequence
if (model.InvoiceDate.Date > DateTime.UtcNow.Date)
{
_logger.LogWarning("Invoice date {InvoiceDate} cannot be in the future.",
model.InvoiceDate);
return BadRequest(ApiResponse<object>.ErrorResponse(
"Invoice date cannot be in the future",
"Invoice date cannot be in the future", 400));
}
if (model.InvoiceDate.Date > model.ClientSubmitedDate.Date)
{
_logger.LogWarning("Invoice date {InvoiceDate} is later than client submitted date {ClientSubmitedDate}",
model.InvoiceDate, model.ClientSubmitedDate);
return BadRequest(ApiResponse<object>.ErrorResponse(
"Invoice date cannot be later than the client submitted date",
"Invoice date is later than client submitted date", 400));
}
if (model.ClientSubmitedDate.Date > DateTime.UtcNow.Date)
{
_logger.LogWarning("Client submited date {ClientSubmitedDate} cannot be in the future.",
model.InvoiceDate);
return BadRequest(ApiResponse<object>.ErrorResponse(
"Client submited date cannot be in the future",
"Client submited date cannot be in the future", 400));
}
if (model.ClientSubmitedDate.Date > model.ExceptedPaymentDate.Date)
{
_logger.LogWarning("Client submitted date {ClientSubmitedDate} is later than expected payment date {ExpectedPaymentDate}",
model.ClientSubmitedDate, model.ExceptedPaymentDate);
return BadRequest(ApiResponse<object>.ErrorResponse(
"Client submission date cannot be later than the expected payment date",
"Client submitted date is later than expected payment date", 400));
}
// Check for existing InvoiceNumber for this tenant before creating/updating to maintain uniqueness.
var invoiceNumberExists = await _context.Invoices
.AnyAsync(i => i.InvoiceNumber == model.InvoiceNumber && i.TenantId == tenantId);
if (invoiceNumberExists)
{
// Log the conflict event with full context for audit/review.
_logger.LogWarning(
"Invoice number conflict detected for InvoiceNumber: {InvoiceNumber} and TenantId: {TenantId}",
model.InvoiceNumber, tenantId);
// Return HTTP 409 (Conflict) with a descriptive, actionable message.
return StatusCode(409, ApiResponse<object>.ErrorResponse(
"Invoice number already exists",
$"The invoice number '{model.InvoiceNumber}' is already in use for this tenant. Please choose a unique invoice number.",
409));
}
// If E-InvoiceNumber is provided (optional), validate its uniqueness for this tenant.
if (!string.IsNullOrWhiteSpace(model.EInvoiceNumber))
{
var eInvoiceNumberExists = await _context.Invoices
.AnyAsync(i => i.EInvoiceNumber == model.EInvoiceNumber && i.TenantId == tenantId);
if (eInvoiceNumberExists)
{
_logger.LogWarning(
"E-Invoice number conflict detected for EInvoiceNumber: {EInvoiceNumber} and TenantId: {TenantId}",
model.EInvoiceNumber, tenantId);
// Return HTTP 409 (Conflict) with a tailored message for E-Invoice.
return StatusCode(409, ApiResponse<object>.ErrorResponse(
"E-Invoice number already exists",
$"The E-Invoice number '{model.EInvoiceNumber}' is already assigned to another invoice for this tenant. Please provide a unique E-Invoice number.",
409));
}
}
// Fetch project
var project = await _context.Projects
.FirstOrDefaultAsync(p => p.Id == model.ProjectId && p.TenantId == tenantId);
if (project == null)
{
_logger.LogWarning("Project not found: ProjectId {ProjectId}, TenantId {TenantId}",
model.ProjectId, tenantId);
return NotFound(ApiResponse<object>.ErrorResponse("Project not found", "Project not found", 404));
}
// Begin transaction scope with async flow support
await using var transaction = await _context.Database.BeginTransactionAsync();
var invoice = new Invoice();
try
{
// Map and create invoice
invoice = _mapper.Map<Invoice>(model);
invoice.IsActive = true;
invoice.MarkAsCompleted = false;
invoice.CreatedAt = DateTime.UtcNow;
invoice.CreatedById = loggedInEmployee.Id;
invoice.TenantId = tenantId;
_context.Invoices.Add(invoice);
await _context.SaveChangesAsync(); // Save to generate invoice.Id
// Handle attachments
var documents = new List<Document>();
var invoiceAttachments = new List<InvoiceAttachment>();
if (model.Attachments?.Any() == true)
{
var batchId = Guid.NewGuid();
foreach (var attachment in model.Attachments)
{
string base64 = attachment.Base64Data?.Split(',').LastOrDefault() ?? "";
if (string.IsNullOrWhiteSpace(base64))
{
_logger.LogWarning("Base64 data is missing for attachment {FileName}", attachment.FileName ?? "");
return BadRequest(ApiResponse<object>.ErrorResponse("Base64 data is missing", "Image data missing", 400));
}
var fileType = _s3Service.GetContentTypeFromBase64(base64);
var fileName = _s3Service.GenerateFileName(fileType, tenantId, "invoice");
var objectKey = $"tenant-{tenantId}/Project/{model.ProjectId}/Invoice/{fileName}";
await _s3Service.UploadFileAsync(base64, fileType, objectKey);
var document = new Document
{
Id = Guid.NewGuid(),
BatchId = batchId,
UploadedById = loggedInEmployee.Id,
FileName = attachment.FileName ?? fileName,
ContentType = attachment.ContentType,
S3Key = objectKey,
FileSize = attachment.FileSize,
UploadedAt = DateTime.UtcNow,
TenantId = tenantId
};
documents.Add(document);
var invoiceAttachment = new InvoiceAttachment
{
InvoiceId = invoice.Id,
DocumentId = document.Id,
TenantId = tenantId
};
invoiceAttachments.Add(invoiceAttachment);
}
_context.Documents.AddRange(documents);
_context.InvoiceAttachments.AddRange(invoiceAttachments);
await _context.SaveChangesAsync(); // Save attachments and mappings
}
// Commit transaction
await transaction.CommitAsync();
_logger.LogInfo("Invoice {InvoiceId} created successfully with {AttachmentCount} attachments.",
invoice.Id, documents.Count);
}
catch (Exception ex)
{
await transaction.RollbackAsync();
_logger.LogError(ex, "Transaction rolled back during invoice creation for ProjectId {ProjectId}", model.ProjectId);
return StatusCode(500, ApiResponse<object>.ErrorResponse(
"Transaction failed: " + ex.Message,
"An error occurred while creating the invoice", 500));
}
// Build response
var response = _mapper.Map<InvoiceListVM>(invoice);
response.Project = _mapper.Map<BasicProjectVM>(project);
response.CreatedBy = _mapper.Map<BasicEmployeeVM>(loggedInEmployee);
response.BalanceAmount = response.BasicAmount + response.TaxAmount;
return StatusCode(201, ApiResponse<object>.SuccessResponse(response, "Invoice Created Successfully", 201));
}
/// <summary>
/// Creates a new received invoice payment record after validating business rules.
/// </summary>
/// <param name="model">The received invoice payment data transfer object containing payment details.</param>
/// <returns>An action result containing the created payment view model or error response.</returns>
[HttpPost("invoice/payment/received")]
public async Task<IActionResult> CreateReceivedInvoicePaymentAsync([FromBody] ReceivedInvoicePaymentDto model)
{
// Validate input model
if (model == null)
{
_logger.LogWarning("Received invoice payment creation request with null model");
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid model", "Request payload cannot be null", 400));
}
await using var _context = await _dbContextFactory.CreateDbContextAsync();
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
// Retrieve invoice with tenant isolation and no tracking for read-only access
var invoice = await _context.Invoices
.AsNoTracking()
.FirstOrDefaultAsync(i => i.Id == model.InvoiceId && i.TenantId == tenantId);
if (invoice == null)
{
_logger.LogWarning("Invoice not found for ID {InvoiceId} and TenantId {TenantId}", model.InvoiceId, tenantId);
return NotFound(ApiResponse<object>.ErrorResponse("Invoice not found", "The specified invoice does not exist", 404));
}
// Check if invoice is already marked as completed
if (invoice.MarkAsCompleted)
{
_logger.LogWarning("Attempt to add payment to completed invoice {InvoiceId}", model.InvoiceId);
return BadRequest(ApiResponse<object>.ErrorResponse(
"Cannot add received payment to completed invoice",
"Payments cannot be added to invoices that are already marked as completed", 400));
}
// Validate payment received date is not in the future
if (model.PaymentReceivedDate.Date > DateTime.UtcNow.Date)
{
_logger.LogWarning("Future payment date {PaymentReceivedDate} provided for invoice {InvoiceId}",
model.PaymentReceivedDate, model.InvoiceId);
return BadRequest(ApiResponse<object>.ErrorResponse(
"Payment received date cannot be in the future",
"The payment received date must not be later than the current date", 400));
}
// Validate client submitted date is not later than payment received date
if (invoice.ClientSubmitedDate.Date > model.PaymentReceivedDate.Date)
{
_logger.LogWarning("Client submitted date {ClientSubmitedDate} is later than payment received date {PaymentReceivedDate} for invoice {InvoiceId}",
invoice.ClientSubmitedDate, model.PaymentReceivedDate, model.InvoiceId);
return BadRequest(ApiResponse<object>.ErrorResponse(
"Client submission date cannot be later than the payment received date",
"The client submission date cannot be later than the payment received date", 400));
}
// Retrieve all previous payments for the given invoice and tenant in a single, efficient query.
var receivedInvoicePayments = await _context.ReceivedInvoicePayments
.Where(rip => rip.InvoiceId == invoice.Id && rip.TenantId == tenantId)
.Select(rip => rip.Amount) // Only select required field for better performance.
.ToListAsync();
// Calculate the sum of all previous payments to determine the total paid so far.
var previousPaidAmount = receivedInvoicePayments.Sum();
var totalPaidAmount = previousPaidAmount + model.Amount;
// Compute the invoice's total amount payable including taxes.
var totalAmount = invoice.BasicAmount + invoice.TaxAmount;
// Business rule validation: Prevent the overpayment scenario.
if (totalPaidAmount > totalAmount)
{
// Log the details for easier debugging and audit trails.
_logger.LogWarning(
"Overpayment attempt detected. InvoiceId: {InvoiceId}, TenantId: {TenantId}, TotalInvoiceAmount: {TotalInvoiceAmount}, PreviousPaidAmount: {PreviousPaidAmount}, AttemptedPayment: {AttemptedPayment}, CalculatedTotalPaid: {TotalPaidAmount}.",
invoice.Id, tenantId, totalAmount, previousPaidAmount, model.Amount, totalPaidAmount);
// Return a bad request response with a clear, actionable error message.
return BadRequest(ApiResponse<object>.ErrorResponse(
"Invalid payment: total paid amount exceeds invoice total.",
$"The total of existing payments ({previousPaidAmount}) plus the new payment ({model.Amount}) would exceed the invoice total ({totalAmount}). Please verify payment details.",
400));
}
try
{
// Map DTO to entity and set creation metadata
var receivedInvoicePayment = _mapper.Map<ReceivedInvoicePayment>(model);
receivedInvoicePayment.CreatedAt = DateTime.UtcNow;
receivedInvoicePayment.CreatedById = loggedInEmployee.Id;
receivedInvoicePayment.TenantId = tenantId;
// Add new payment record and save changes
_context.ReceivedInvoicePayments.Add(receivedInvoicePayment);
await _context.SaveChangesAsync();
// Map entity to view model for response
var response = _mapper.Map<ReceivedInvoicePaymentVM>(receivedInvoicePayment);
_logger.LogInfo("Successfully created received payment {PaymentId} for invoice {InvoiceId}",
receivedInvoicePayment.Id, model.InvoiceId);
return StatusCode(201, ApiResponse<object>.SuccessResponse(response, "Payment invoice received successfully", 201));
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occurred while creating received payment for invoice {InvoiceId}", model.InvoiceId);
return StatusCode(500, ApiResponse<object>.ErrorResponse(
"Internal server error",
"An unexpected error occurred while processing the request", 500));
}
}
/// <summary>
/// Adds a comment to the specified invoice, validating model and invoice existence.
/// </summary>
/// <param name="model">DTO containing InvoiceId and Comment text.</param>
/// <returns>201 Created with comment details, or error codes for validation/invoice not found.</returns>
[HttpPost("invoice/add/comment")]
public async Task<IActionResult> AddCommentToInvoiceAsync([FromBody] InvoiceCommentDto model)
{
// Validate incoming data early to avoid unnecessary database calls.
if (string.IsNullOrWhiteSpace(model.Comment))
{
_logger.LogWarning("Invalid or missing comment data for InvoiceId {InvoiceId}, TenantId {TenantId}", model.InvoiceId, tenantId);
return BadRequest(ApiResponse<object>.ErrorResponse(
"Invalid comment data",
"The comment text and model must not be null or empty.",
400));
}
await using var _context = await _dbContextFactory.CreateDbContextAsync();
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
// Find the target invoice for the specified tenant.
var invoice = await _context.Invoices
.FirstOrDefaultAsync(i => i.Id == model.InvoiceId && i.TenantId == tenantId);
if (invoice == null)
{
_logger.LogWarning("Cannot add comment—invoice not found. InvoiceId {InvoiceId}, TenantId {TenantId}", model.InvoiceId, tenantId);
return NotFound(ApiResponse<object>.ErrorResponse(
"Invoice not found",
$"Invoice with ID '{model.InvoiceId}' does not exist for the specified tenant.",
404));
}
// Construct the new comment entity with required audit metadata.
var comment = new InvoiceComment
{
Id = Guid.NewGuid(),
Comment = model.Comment.Trim(),
InvoiceId = model.InvoiceId,
CreatedAt = DateTime.UtcNow,
CreatedById = loggedInEmployee.Id,
TenantId = tenantId
};
_context.InvoiceComments.Add(comment);
await _context.SaveChangesAsync();
_logger.LogInfo("Added new comment to invoice {InvoiceId} by employee {EmployeeId}, TenantId {TenantId}",
comment.InvoiceId, loggedInEmployee.Id, tenantId);
var response = _mapper.Map<InvoiceCommentVM>(comment);
// Return successful creation with comment details.
return StatusCode(201, ApiResponse<object>.SuccessResponse(
response,
"Comment added to invoice successfully.",
201));
}
#endregion
#region =================================================================== Put Functions ===================================================================
/// <summary>
/// Updates an existing invoice if it exists, has no payments, and the model is valid.
/// </summary>
/// <param name="id">The unique identifier of the invoice to update.</param>
/// <param name="model">The updated invoice data transfer object.</param>
/// <returns>Success response on update, or appropriate error if validation fails.</returns>
[HttpPut("invoice/edit/{id}")]
public async Task<IActionResult> UpdateInvoiceAsync(Guid id, [FromBody] InvoiceDto model)
{
// Validate route and model ID consistency
if (!model.Id.HasValue || id != model.Id)
{
_logger.LogWarning("Invoice ID mismatch: route ID {RouteId} does not match model ID {ModelId}", id, model.Id ?? Guid.Empty);
return BadRequest(ApiResponse<object>.ErrorResponse(
"Invalid invoice ID",
"The invoice ID in the URL does not match the ID in the request body.",
400));
}
await using var _context = await _dbContextFactory.CreateDbContextAsync();
using var scope = _serviceScopeFactory.CreateScope();
var _updateLogHelper = scope.ServiceProvider.GetRequiredService<UtilityMongoDBHelper>();
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
// Retrieve the invoice with tenant isolation
var invoice = await _context.Invoices
.FirstOrDefaultAsync(i => i.Id == id && i.TenantId == tenantId);
if (invoice == null)
{
_logger.LogWarning("Invoice not found for ID {InvoiceId} and TenantId {TenantId}", id, tenantId);
return NotFound(ApiResponse<object>.ErrorResponse(
"Invoice not found",
"The specified invoice does not exist for this tenant.",
404));
}
// Fetch project
var project = await _context.Projects
.FirstOrDefaultAsync(p => p.Id == model.ProjectId && p.TenantId == tenantId);
if (project == null)
{
_logger.LogWarning("Project not found: ProjectId {ProjectId}, TenantId {TenantId}",
model.ProjectId, tenantId);
return NotFound(ApiResponse<object>.ErrorResponse("Project not found", "Project not found", 404));
}
// Prevent modification if any payment has already been received
var receivedPaymentExists = await _context.ReceivedInvoicePayments
.AnyAsync(rip => rip.InvoiceId == id && rip.TenantId == tenantId);
if (receivedPaymentExists)
{
_logger.LogWarning("Update blocked: Payment already received for InvoiceId {InvoiceId}, TenantId {TenantId}", id, tenantId);
return BadRequest(ApiResponse<object>.ErrorResponse(
"Invoice cannot be updated",
"This invoice has received payments and cannot be modified.",
400));
}
try
{
var invoiceStateBeforeChange = _updateLogHelper.EntityToBsonDocument(invoice);
// Map updated data to existing invoice entity
_mapper.Map(model, invoice);
invoice.UpdatedAt = DateTime.UtcNow;
invoice.UpdatedById = loggedInEmployee.Id;
// Handle attachment updates if provided
if (model.Attachments?.Any() ?? false)
{
var inactiveDocumentIds = model.Attachments
.Where(a => !a.IsActive && a.DocumentId.HasValue)
.Select(a => a.DocumentId!.Value)
.ToList();
var newAttachments = model.Attachments
.Where(a => a.IsActive && !string.IsNullOrWhiteSpace(a.Base64Data))
.ToList();
// Remove inactive attachments
if (inactiveDocumentIds.Any())
{
var existingInvoiceAttachments = await _context.InvoiceAttachments
.AsNoTracking()
.Where(ia => inactiveDocumentIds.Contains(ia.DocumentId) && ia.TenantId == tenantId)
.ToListAsync();
_context.InvoiceAttachments.RemoveRange(existingInvoiceAttachments);
_logger.LogInfo("Removed {Count} inactive attachments for InvoiceId {InvoiceId}", existingInvoiceAttachments.Count, id);
}
// Process and upload new attachments
if (newAttachments.Any())
{
var batchId = Guid.NewGuid();
var documents = new List<Document>();
var invoiceAttachments = new List<InvoiceAttachment>();
foreach (var attachment in newAttachments)
{
string base64Data = attachment.Base64Data?.Split(',').LastOrDefault() ?? string.Empty;
if (string.IsNullOrWhiteSpace(base64Data))
{
_logger.LogWarning("Base64 data missing for attachment: {FileName}", attachment.FileName ?? "Unknown");
return BadRequest(ApiResponse<object>.ErrorResponse(
"Invalid attachment data",
"Base64 data is missing or malformed for one or more attachments.",
400));
}
var contentType = _s3Service.GetContentTypeFromBase64(base64Data);
var fileName = _s3Service.GenerateFileName(contentType, tenantId, "invoice");
var objectKey = $"tenant-{tenantId}/Project/{model.ProjectId}/Invoice/{fileName}";
// Upload file to S3
await _s3Service.UploadFileAsync(base64Data, contentType, objectKey);
// Create document record
var document = new Document
{
Id = Guid.NewGuid(),
BatchId = batchId,
UploadedById = loggedInEmployee.Id,
FileName = attachment.FileName ?? fileName,
ContentType = contentType,
S3Key = objectKey,
FileSize = attachment.FileSize,
UploadedAt = DateTime.UtcNow,
TenantId = tenantId
};
documents.Add(document);
// Link document to invoice
var invoiceAttachment = new InvoiceAttachment
{
InvoiceId = invoice.Id,
DocumentId = document.Id,
TenantId = tenantId
};
invoiceAttachments.Add(invoiceAttachment);
}
_context.Documents.AddRange(documents);
_context.InvoiceAttachments.AddRange(invoiceAttachments);
_logger.LogInfo("Added {Count} new attachments to InvoiceId {InvoiceId}", invoiceAttachments.Count, id);
}
}
// Save all changes in a single transaction
await _context.SaveChangesAsync();
await _updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject
{
EntityId = invoice.Id.ToString(),
UpdatedById = loggedInEmployee.Id.ToString(),
OldObject = invoiceStateBeforeChange,
UpdatedAt = DateTime.UtcNow
}, "InvoiceModificationLog");
_logger.LogInfo("Invoice {InvoiceId} updated successfully by EmployeeId {EmployeeId}, TenantId {TenantId}",
invoice.Id, loggedInEmployee.Id, tenantId);
// Build response
var response = _mapper.Map<InvoiceListVM>(invoice);
response.Project = _mapper.Map<BasicProjectVM>(project);
response.UpdatedBy = _mapper.Map<BasicEmployeeVM>(loggedInEmployee);
response.BalanceAmount = response.BasicAmount + response.TaxAmount;
return Ok(ApiResponse<object>.SuccessResponse(response, "Invoice updated successfully", 200));
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occurred while updating InvoiceId {InvoiceId}, TenantId {TenantId}", id, tenantId);
return StatusCode(500, ApiResponse<object>.ErrorResponse(
"Internal server error",
"An unexpected error occurred while updating the invoice.",
500));
}
}
/// <summary>
/// Marks the specified invoice as completed if it exists and is not already completed.
/// </summary>
/// <param name="invoiceId">The unique identifier of the invoice to mark as completed.</param>
/// <returns>An action result indicating success or the nature of the error.</returns>
[HttpPut("invoice/marked/completed/{invoiceId}")]
public async Task<IActionResult> MarkAsCompletedAsync(Guid invoiceId)
{
// Create a new async database context for the current request's scope.
await using var _context = await _dbContextFactory.CreateDbContextAsync();
// Retrieve the current logged in employee for audit/logging (optional use).
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
// Attempt to find the invoice with tenant isolation; use AsNoTracking if no updates needed (but here we update so tracking is okay).
var invoice = await _context.Invoices
.FirstOrDefaultAsync(i => i.Id == invoiceId && i.TenantId == tenantId);
// Log and return 404 if the invoice does not exist.
if (invoice == null)
{
_logger.LogWarning("Invoice not found for ID {InvoiceId} and TenantId {TenantId}", invoiceId, tenantId);
return NotFound(ApiResponse<object>.ErrorResponse("Invoice not found", "The specified invoice does not exist", 404));
}
// If the invoice is already marked as completed, log and return meaningful error.
if (invoice.MarkAsCompleted)
{
_logger.LogWarning("Attempt to mark already completed invoice {InvoiceId} as completed by user {UserId}", invoiceId, loggedInEmployee.Id);
return BadRequest(ApiResponse<object>.ErrorResponse(
"Invoice already completed",
"Invoice is already marked as completed", 400));
}
try
{
// Mark invoice as completed.
invoice.MarkAsCompleted = true;
// Persist the change to the database.
await _context.SaveChangesAsync();
_logger.LogInfo("Invoice {InvoiceId} marked as completed by user {UserId}", invoiceId, loggedInEmployee.Id);
return Ok(ApiResponse<object>.SuccessResponse(new { }, "Invoice is marked as completed successfully", 200));
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occurred while marking invoice {InvoiceId} as completed by user {UserId}", invoiceId, loggedInEmployee.Id);
return StatusCode(500, ApiResponse<object>.ErrorResponse(
"Internal server error",
"An unexpected error occurred while processing the request", 500));
}
}
#endregion
#region =================================================================== Helper Functions ===================================================================
/// <summary>
/// Loads invoice comments asynchronously with related metadata.
/// </summary>
private async Task<List<InvoiceComment>> LoadInvoiceCommentsAsync(Guid invoiceId, Guid tenantId)
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.InvoiceComments
.Include(ic => ic.CreatedBy).ThenInclude(e => e!.JobRole)
.AsNoTracking()
.Where(ic => ic.InvoiceId == invoiceId && ic.TenantId == tenantId)
.ToListAsync();
}
/// <summary>
/// Loads invoice attachments and their upload metadata asynchronously.
/// </summary>
private async Task<List<InvoiceAttachment>> LoadInvoiceAttachmentsAsync(Guid invoiceId, Guid tenantId)
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.InvoiceAttachments
.Include(ia => ia.Document)
.ThenInclude(d => d!.UploadedBy)
.ThenInclude(e => e!.JobRole)
.AsNoTracking()
.Where(ia => ia.InvoiceId == invoiceId && ia.TenantId == tenantId && ia.Document != null && ia.Document.UploadedBy != null)
.ToListAsync();
}
/// <summary>
/// Loads received invoice payment records asynchronously with creator metadata.
/// </summary>
private async Task<List<ReceivedInvoicePayment>> LoadReceivedInvoicePaymentsAsync(Guid invoiceId, Guid tenantId)
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.ReceivedInvoicePayments
.Include(rip => rip.PaymentAdjustmentHead)
.Include(rip => rip.CreatedBy).ThenInclude(e => e!.JobRole)
.AsNoTracking()
.Where(rip => rip.InvoiceId == invoiceId && rip.TenantId == tenantId)
.ToListAsync();
}
#endregion
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,6 @@
using Marco.Pms.DataAccess.Data; using Marco.Pms.DataAccess.Data;
using Marco.Pms.Model.Dtos.Activities; using Marco.Pms.Model.Dtos.Activities;
using Marco.Pms.Model.Dtos.Collection;
using Marco.Pms.Model.Dtos.DocumentManager; using Marco.Pms.Model.Dtos.DocumentManager;
using Marco.Pms.Model.Dtos.Master; using Marco.Pms.Model.Dtos.Master;
using Marco.Pms.Model.Forum; using Marco.Pms.Model.Forum;
@ -975,5 +976,38 @@ namespace Marco.Pms.Services.Controllers
} }
#endregion #endregion
#region =================================================================== Payment Adjustment Head APIs ===================================================================
[HttpGet("payment-adjustment-head/list")]
public async Task<IActionResult> GetpaymentAdjustmentHeadsList([FromQuery] bool isActive = true)
{
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var response = await _masterService.GetPaymentAdjustmentHeadListAsync(isActive, loggedInEmployee, tenantId);
return StatusCode(response.StatusCode, response);
}
[HttpPost("payment-adjustment-head")]
public async Task<IActionResult> CreatePaymentAdjustmentHead([FromBody] PaymentAdjustmentHeadDto dto)
{
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var response = await _masterService.CreatePaymentAdjustmentHeadAsync(dto, loggedInEmployee, tenantId);
return StatusCode(response.StatusCode, response);
}
[HttpPut("payment-adjustment-head/edit/{id}")]
public async Task<IActionResult> UpdatePaymentAdjustmentHead(Guid id, [FromBody] PaymentAdjustmentHeadDto dto)
{
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var response = await _masterService.UpdatePaymentAdjustmentHeadAsync(id, dto, loggedInEmployee, tenantId);
return StatusCode(response.StatusCode, response);
}
[HttpDelete("payment-adjustment-head/delete/{id}")]
public async Task<IActionResult> DeletePaymentAdjustmentHead(Guid id, [FromQuery] bool isActive = false)
{
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var response = await _masterService.DeletePaymentAdjustmentHeadAsync(id, isActive, loggedInEmployee, tenantId);
return StatusCode(response.StatusCode, response);
}
#endregion
} }
} }

View File

@ -41,11 +41,11 @@ namespace MarcoBMS.Services.Controllers
#region =================================================================== Project Get APIs =================================================================== #region =================================================================== Project Get APIs ===================================================================
[HttpGet("list/basic")] [HttpGet("list/basic")]
public async Task<IActionResult> GetAllProjectsBasic() public async Task<IActionResult> GetAllProjectsBasic([FromQuery] bool provideAll = false)
{ {
// Get the current user // Get the current user
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var response = await _projectServices.GetAllProjectsBasicAsync(tenantId, loggedInEmployee); var response = await _projectServices.GetAllProjectsBasicAsync(provideAll, tenantId, loggedInEmployee);
return StatusCode(response.StatusCode, response); return StatusCode(response.StatusCode, response);
} }

View File

@ -24,7 +24,7 @@ namespace MarcoBMS.Services.Helpers
public async Task<Employee> GetEmployeeByID(Guid EmployeeID) public async Task<Employee> GetEmployeeByID(Guid EmployeeID)
{ {
return await _context.Employees.Include(e => e.JobRole).FirstOrDefaultAsync(e => e.Id == EmployeeID) ?? new Employee { }; return await _context.Employees.Include(e => e.JobRole).Include(e => e.Organization).FirstOrDefaultAsync(e => e.Id == EmployeeID) ?? new Employee { };
} }
public async Task<Employee> GetEmployeeByApplicationUserID(string ApplicationUserID) public async Task<Employee> GetEmployeeByApplicationUserID(string ApplicationUserID)

View File

@ -1,9 +1,11 @@
using AutoMapper; using AutoMapper;
using Marco.Pms.Model.AppMenu; using Marco.Pms.Model.AppMenu;
using Marco.Pms.Model.Collection;
using Marco.Pms.Model.Directory; using Marco.Pms.Model.Directory;
using Marco.Pms.Model.DocumentManager; using Marco.Pms.Model.DocumentManager;
using Marco.Pms.Model.Dtos.Activities; using Marco.Pms.Model.Dtos.Activities;
using Marco.Pms.Model.Dtos.AppMenu; using Marco.Pms.Model.Dtos.AppMenu;
using Marco.Pms.Model.Dtos.Collection;
using Marco.Pms.Model.Dtos.Directory; using Marco.Pms.Model.Dtos.Directory;
using Marco.Pms.Model.Dtos.DocumentManager; using Marco.Pms.Model.Dtos.DocumentManager;
using Marco.Pms.Model.Dtos.Employees; using Marco.Pms.Model.Dtos.Employees;
@ -26,6 +28,7 @@ using Marco.Pms.Model.Projects;
using Marco.Pms.Model.TenantModels; using Marco.Pms.Model.TenantModels;
using Marco.Pms.Model.TenantModels.MongoDBModel; using Marco.Pms.Model.TenantModels.MongoDBModel;
using Marco.Pms.Model.ViewModels.Activities; using Marco.Pms.Model.ViewModels.Activities;
using Marco.Pms.Model.ViewModels.Collection;
using Marco.Pms.Model.ViewModels.Directory; using Marco.Pms.Model.ViewModels.Directory;
using Marco.Pms.Model.ViewModels.DocumentManager; using Marco.Pms.Model.ViewModels.DocumentManager;
using Marco.Pms.Model.ViewModels.Employee; using Marco.Pms.Model.ViewModels.Employee;
@ -255,6 +258,19 @@ namespace Marco.Pms.Services.MappingProfiles
#endregion #endregion
#region ======================================================= Collection =======================================================
CreateMap<InvoiceDto, Invoice>();
CreateMap<Invoice, InvoiceListVM>();
CreateMap<Invoice, InvoiceDetailsVM>();
CreateMap<ReceivedInvoicePaymentDto, ReceivedInvoicePayment>();
CreateMap<ReceivedInvoicePayment, ReceivedInvoicePaymentVM>();
CreateMap<InvoiceComment, InvoiceCommentVM>();
CreateMap<InvoiceAttachment, InvoiceAttachmentVM>();
#endregion
#region ======================================================= Master ======================================================= #region ======================================================= Master =======================================================
CreateMap<FeaturePermission, FeaturePermissionVM>(); CreateMap<FeaturePermission, FeaturePermissionVM>();
@ -384,19 +400,27 @@ namespace Marco.Pms.Services.MappingProfiles
CreateMap<UpdateContactCategoryDto, ContactCategoryMaster>(); CreateMap<UpdateContactCategoryDto, ContactCategoryMaster>();
CreateMap<ContactCategoryMaster, ContactCategoryVM>(); CreateMap<ContactCategoryMaster, ContactCategoryVM>();
#endregion #endregion
#region ======================================================= Contact Tag Master ======================================================= #region ======================================================= Contact Tag Master =======================================================
CreateMap<CreateContactTagDto, ContactTagMaster>(); CreateMap<CreateContactTagDto, ContactTagMaster>();
CreateMap<UpdateContactTagDto, ContactTagMaster>(); CreateMap<UpdateContactTagDto, ContactTagMaster>();
CreateMap<ContactTagMaster, ContactTagVM>(); CreateMap<ContactTagMaster, ContactTagVM>();
#endregion #endregion
#region ======================================================= Payment Adjustment Head Master =======================================================
CreateMap<PaymentAdjustmentHeadDto, PaymentAdjustmentHead>();
CreateMap<PaymentAdjustmentHead, PaymentAdjustmentHeadVM>();
#endregion
#region ======================================================= Expenses Status Master ======================================================= #region ======================================================= Expenses Status Master =======================================================
#endregion #endregion
#region ======================================================= Expenses Status Master ======================================================= #region ======================================================= Expenses Status Master =======================================================
#endregion #endregion
#region ======================================================= Expenses Status Master ======================================================= #region ======================================================= Expenses Status Master =======================================================
#endregion #endregion
#region ======================================================= Expenses Status Master =======================================================
#endregion
#region ======================================================= Expenses Status Master ======================================================= #region ======================================================= Expenses Status Master =======================================================
#endregion #endregion

View File

@ -1,9 +1,11 @@
using AutoMapper; using AutoMapper;
using Marco.Pms.DataAccess.Data; using Marco.Pms.DataAccess.Data;
using Marco.Pms.Helpers.Utility; using Marco.Pms.Helpers.Utility;
using Marco.Pms.Model.Collection;
using Marco.Pms.Model.Directory; using Marco.Pms.Model.Directory;
using Marco.Pms.Model.DocumentManager; using Marco.Pms.Model.DocumentManager;
using Marco.Pms.Model.Dtos.Activities; using Marco.Pms.Model.Dtos.Activities;
using Marco.Pms.Model.Dtos.Collection;
using Marco.Pms.Model.Dtos.DocumentManager; using Marco.Pms.Model.Dtos.DocumentManager;
using Marco.Pms.Model.Dtos.Master; using Marco.Pms.Model.Dtos.Master;
using Marco.Pms.Model.Employees; using Marco.Pms.Model.Employees;
@ -12,6 +14,7 @@ using Marco.Pms.Model.Master;
using Marco.Pms.Model.MongoDBModels.Utility; using Marco.Pms.Model.MongoDBModels.Utility;
using Marco.Pms.Model.Utilities; using Marco.Pms.Model.Utilities;
using Marco.Pms.Model.ViewModels.Activities; using Marco.Pms.Model.ViewModels.Activities;
using Marco.Pms.Model.ViewModels.Collection;
using Marco.Pms.Model.ViewModels.DocumentManager; using Marco.Pms.Model.ViewModels.DocumentManager;
using Marco.Pms.Model.ViewModels.Master; using Marco.Pms.Model.ViewModels.Master;
using Marco.Pms.Services.Service.ServiceInterfaces; using Marco.Pms.Services.Service.ServiceInterfaces;
@ -2652,8 +2655,6 @@ namespace Marco.Pms.Services.Service
#endregion #endregion
#region =================================================================== Document Type APIs =================================================================== #region =================================================================== Document Type APIs ===================================================================
public async Task<ApiResponse<object>> GetDocumentTypeMasterListAsync(Guid? documentCategoryId, Employee loggedInEmployee, Guid tenantId) public async Task<ApiResponse<object>> GetDocumentTypeMasterListAsync(Guid? documentCategoryId, Employee loggedInEmployee, Guid tenantId)
{ {
try try
@ -2881,6 +2882,248 @@ namespace Marco.Pms.Services.Service
#endregion #endregion
#region =================================================================== Payment Adjustment Head APIs ===================================================================
/// <summary>
/// Retrieves a list of payment adjustment heads for a specific tenant with optional active status filtering.
/// </summary>
/// <param name="isActive">Filter for active/inactive payment adjustment heads</param>
/// <param name="loggedInEmployee">The employee making the request (for auditing/authorization)</param>
/// <param name="tenantId">The tenant identifier to scope the data</param>
/// <returns>An API response containing the list of payment adjustment head view models</returns>
/// <remarks>
/// This method performs database-level filtering and uses projection to minimize data transfer.
/// Consider implementing pagination for tenants with large numbers of payment adjustment heads.
/// </remarks>
public async Task<ApiResponse<object>> GetPaymentAdjustmentHeadListAsync(bool isActive, Employee loggedInEmployee, Guid tenantId)
{
try
{
// Log the request details for auditing and troubleshooting
_logger.LogInfo("Fetching payment adjustment heads for tenant {TenantId} with IsActive={IsActive}", tenantId, isActive);
var paymentAdjustmentHeads = await _context.PaymentAdjustmentHeads
.AsNoTracking() // Improve performance by disabling change tracking for read-only operations
.Where(pah => pah.TenantId == tenantId && pah.IsActive == isActive)
.Select(pah => _mapper.Map<PaymentAdjustmentHeadVM>(pah))
.ToListAsync();
_logger.LogInfo("Successfully retrieved {Count} payment adjustment heads for tenant {TenantId}", paymentAdjustmentHeads.Count, tenantId);
return ApiResponse<object>.SuccessResponse(
paymentAdjustmentHeads.OrderBy(pah => pah.Name).ToList(),
$"Payment Adjustment Heads fetched successfully. Count: {paymentAdjustmentHeads.Count}",
200);
}
catch (Exception ex)
{
// Log the full exception with context for better troubleshooting
_logger.LogError(ex, "Error occurred while fetching payment adjustment heads for tenant {TenantId}. IsActive: {IsActive}", tenantId, isActive);
return ApiResponse<object>.ErrorResponse("An error occurred", "Unable to fetch payment adjustment heads", 500);
}
}
/// <summary>
/// Creates a new payment adjustment head for the specified tenant after permission and uniqueness checks.
/// </summary>
/// <param name="model">DTO containing payment adjustment head data</param>
/// <param name="loggedInEmployee">The employee performing the action</param>
/// <param name="tenantId">The tenant identifier</param>
/// <returns>API response with status and context</returns>
public async Task<ApiResponse<object>> CreatePaymentAdjustmentHeadAsync(PaymentAdjustmentHeadDto model, Employee loggedInEmployee, Guid tenantId)
{
try
{
// Permission validation
var hasManagePermission = await _permission.HasPermission(PermissionsMaster.ManageMasters, loggedInEmployee.Id);
if (!hasManagePermission)
{
_logger.LogWarning("Access denied for employee {EmployeeId} attempting to create payment adjustment head for tenant {TenantId}.", loggedInEmployee.Id, tenantId);
return ApiResponse<object>.ErrorResponse("Access Denied.", "You do not have permission to manage masters.", 403);
}
// Uniqueness check for payment adjustment head name
var nameExists = await _context.PaymentAdjustmentHeads
.AnyAsync(pah => pah.Name == model.Name && pah.TenantId == tenantId);
if (nameExists)
{
_logger.LogInfo("Duplicate payment adjustment head name '{Name}' detected for tenant {TenantId}.", model.Name, tenantId);
return ApiResponse<object>.ErrorResponse("A payment adjustment head with this name already exists.", "Name of payment adjustment head already exists.", 409);
}
// Create and persist new entity
var paymentAdjustmentHead = _mapper.Map<PaymentAdjustmentHead>(model);
paymentAdjustmentHead.IsActive = true;
paymentAdjustmentHead.TenantId = tenantId;
_context.PaymentAdjustmentHeads.Add(paymentAdjustmentHead);
await _context.SaveChangesAsync();
var response = _mapper.Map<PaymentAdjustmentHeadVM>(paymentAdjustmentHead);
_logger.LogInfo("Payment adjustment head '{Name}' created successfully by employee {EmployeeId} for tenant {TenantId}.", paymentAdjustmentHead.Name, loggedInEmployee.Id, tenantId);
return ApiResponse<object>.SuccessResponse(response, "Payment adjustment head created successfully.", 201);
}
catch (Exception ex)
{
// Log full context with exception for easier debugging
_logger.LogError(ex, "Exception while creating payment adjustment head. Employee: {EmployeeId}, Tenant: {TenantId}, Data: {@Model}", loggedInEmployee.Id, tenantId, model);
return ApiResponse<object>.ErrorResponse("An error occurred.", "Unable to create payment adjustment head at this moment.", 500);
}
}
/// <summary>
/// Updates an existing payment adjustment head for a specified tenant after performing
/// necessary validation, permission, and uniqueness checks.
/// </summary>
/// <param name="id">Unique identifier of the payment adjustment head to update</param>
/// <param name="model">DTO containing updated payment adjustment head data</param>
/// <param name="loggedInEmployee">The employee performing the action</param>
/// <param name="tenantId">The tenant identifier</param>
/// <returns>API response object with update result and status message</returns>
public async Task<ApiResponse<object>> UpdatePaymentAdjustmentHeadAsync(Guid id, PaymentAdjustmentHeadDto model, Employee loggedInEmployee, Guid tenantId)
{
try
{
// --- Step 1: Validate request payload correctness ---
if (!model.Id.HasValue || model.Id != id)
{
_logger.LogWarning("Invalid ID provided in request. Model ID: {ModelId}, Route ID: {RouteId}, TenantId: {TenantId}", model.Id ?? Guid.Empty, id, tenantId);
return ApiResponse<object>.ErrorResponse("Invalid request.", "Provided invalid ID.", 400);
}
// --- Step 2: Validate permissions ---
var hasManagePermission = await _permission.HasPermission(PermissionsMaster.ManageMasters, loggedInEmployee.Id);
if (!hasManagePermission)
{
_logger.LogWarning("Access denied for employee {EmployeeId} attempting to update payment adjustment head for tenant {TenantId}.", loggedInEmployee.Id, tenantId);
return ApiResponse<object>.ErrorResponse("Access Denied.", "You do not have permission to manage masters.", 403);
}
// --- Step 3: Validate uniqueness constraint for name ---
var nameExists = await _context.PaymentAdjustmentHeads
.AnyAsync(pah => pah.Name == model.Name && pah.Id != id && pah.TenantId == tenantId);
if (nameExists)
{
_logger.LogInfo("Duplicate payment adjustment head name '{Name}' detected during update for tenant {TenantId}.", model.Name, tenantId);
return ApiResponse<object>.ErrorResponse("Conflict detected.", "A payment adjustment head with this name already exists.", 409);
}
// --- Step 4: Retrieve and validate existing entity ---
var paymentAdjustmentHead = await _context.PaymentAdjustmentHeads
.FirstOrDefaultAsync(pah => pah.Id == id && pah.TenantId == tenantId);
if (paymentAdjustmentHead == null)
{
_logger.LogWarning("Payment adjustment head with ID {Id} not found for tenant {TenantId}.", id, tenantId);
return ApiResponse<object>.ErrorResponse("Not Found.", "Payment adjustment head not found.", 404);
}
// Mapping PaymentAdjustmentHead to BsonDocument
var existingEntityBson = _updateLogHelper.EntityToBsonDocument(paymentAdjustmentHead);
// --- Step 5: Map changes and update entity ---
_mapper.Map(model, paymentAdjustmentHead);
await _context.SaveChangesAsync();
await _updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject
{
EntityId = paymentAdjustmentHead.Id.ToString(),
UpdatedById = loggedInEmployee.Id.ToString(),
OldObject = existingEntityBson,
UpdatedAt = DateTime.UtcNow
}, "PaymentAdjustmentHeadModificationLog");
_logger.LogInfo("Payment adjustment head '{Name}' updated successfully by employee {EmployeeId} for tenant {TenantId}.", paymentAdjustmentHead.Name, loggedInEmployee.Id, tenantId);
var response = _mapper.Map<PaymentAdjustmentHeadVM>(paymentAdjustmentHead);
// --- Step 6: Return structured success response ---
return ApiResponse<object>.SuccessResponse(response, "Payment adjustment head updated successfully.", 200);
}
catch (Exception ex)
{
// --- Step 7: Handle and log exceptions with full context ---
_logger.LogError(ex, "Exception while updating payment adjustment head. Employee: {EmployeeId}, Tenant: {TenantId}, Model: {@Model}", loggedInEmployee.Id, tenantId, model);
return ApiResponse<object>.ErrorResponse("An unexpected error occurred.", "Unable to update payment adjustment head at this moment.", 500);
}
}
/// <summary>
/// Activates or deactivates a payment adjustment head (soft delete/restore) for a tenant,
/// including audit logging and permission validation.
/// </summary>
/// <param name="id">Unique identifier of the payment adjustment head</param>
/// <param name="isActive">Flag indicating activation (restore) or deactivation (delete)</param>
/// <param name="loggedInEmployee">Employee requesting the operation</param>
/// <param name="tenantId">The tenant identifier</param>
/// <returns>API response object with operation status</returns>
public async Task<ApiResponse<object>> DeletePaymentAdjustmentHeadAsync(Guid id, bool isActive, Employee loggedInEmployee, Guid tenantId)
{
// Dynamically select operation word for logs and messages
var operation = isActive ? "restore" : "delete";
try
{
// Step 1: Permission check
var hasManagePermission = await _permission.HasPermission(PermissionsMaster.ManageMasters, loggedInEmployee.Id);
if (!hasManagePermission)
{
_logger.LogWarning("Access denied: Employee {EmployeeId} attempted to {Operation} payment adjustment head for tenant {TenantId}.",
loggedInEmployee.Id, operation, tenantId);
return ApiResponse<object>.ErrorResponse("Access Denied.", "You do not have permission to manage masters.", 403);
}
// Step 2: Entity existence check
var paymentAdjustmentHead = await _context.PaymentAdjustmentHeads
.FirstOrDefaultAsync(pah => pah.Id == id && pah.TenantId == tenantId);
if (paymentAdjustmentHead == null)
{
_logger.LogWarning("Payment adjustment head with ID {Id} not found for tenant {TenantId} (attempted {Operation}).",
id, tenantId, operation);
return ApiResponse<object>.ErrorResponse("Not Found.", "Payment adjustment head not found.", 404);
}
// Step 3: Save pre-update state for audit log
var existingEntityBson = _updateLogHelper.EntityToBsonDocument(paymentAdjustmentHead);
// Step 4: Update IsActive status
paymentAdjustmentHead.IsActive = isActive;
await _context.SaveChangesAsync();
// Step 5: Push update action to audit log
await _updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject
{
EntityId = paymentAdjustmentHead.Id.ToString(),
UpdatedById = loggedInEmployee.Id.ToString(),
OldObject = existingEntityBson,
UpdatedAt = DateTime.UtcNow
}, "PaymentAdjustmentHeadModificationLog");
_logger.LogInfo(
"Payment adjustment head (ID: {Id}, Name: {Name}) successfully {Operation}d by employee {EmployeeId} for tenant {TenantId}.",
paymentAdjustmentHead.Id, paymentAdjustmentHead.Name, operation, loggedInEmployee.Id, tenantId);
return ApiResponse<object>.SuccessResponse(new { }, $"Payment adjustment head {operation}d successfully.", 200);
}
catch (Exception ex)
{
_logger.LogError(ex,
"Exception occurred while performing {Operation} on payment adjustment head (ID: {Id}) for tenant {TenantId}.",
operation, id, tenantId);
return ApiResponse<object>.ErrorResponse("An error occurred.", $"Exception occurred while trying to {operation} payment adjustment head.", 500);
}
}
#endregion
#region =================================================================== Helper Function =================================================================== #region =================================================================== Helper Function ===================================================================
private static object ExceptionMapper(Exception ex) private static object ExceptionMapper(Exception ex)
{ {

View File

@ -54,7 +54,7 @@ namespace Marco.Pms.Services.Service
#region =================================================================== Project Get APIs =================================================================== #region =================================================================== Project Get APIs ===================================================================
public async Task<ApiResponse<object>> GetAllProjectsBasicAsync(Guid tenantId, Employee loggedInEmployee) public async Task<ApiResponse<object>> GetAllProjectsBasicAsync(bool provideAll, Guid tenantId, Employee loggedInEmployee)
{ {
try try
{ {
@ -67,7 +67,15 @@ namespace Marco.Pms.Services.Service
_logger.LogInfo("Basic project list requested by EmployeeId {EmployeeId}", loggedInEmployee.Id); _logger.LogInfo("Basic project list requested by EmployeeId {EmployeeId}", loggedInEmployee.Id);
// Step 2: Get the list of project IDs the user has access to // Step 2: Get the list of project IDs the user has access to
List<Guid> accessibleProjectIds = await GetMyProjects(tenantId, loggedInEmployee); List<Guid> accessibleProjectIds = new List<Guid>();
if (provideAll)
{
accessibleProjectIds = await _context.Projects.Where(p => p.TenantId == tenantId).Select(p => p.Id).ToListAsync();
}
else
{
accessibleProjectIds = await GetMyProjects(tenantId, loggedInEmployee);
}
if (accessibleProjectIds == null || !accessibleProjectIds.Any()) if (accessibleProjectIds == null || !accessibleProjectIds.Any())
{ {

View File

@ -1,4 +1,5 @@
using Marco.Pms.Model.Dtos.Activities; using Marco.Pms.Model.Dtos.Activities;
using Marco.Pms.Model.Dtos.Collection;
using Marco.Pms.Model.Dtos.DocumentManager; using Marco.Pms.Model.Dtos.DocumentManager;
using Marco.Pms.Model.Dtos.Master; using Marco.Pms.Model.Dtos.Master;
using Marco.Pms.Model.Employees; using Marco.Pms.Model.Employees;
@ -46,6 +47,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces
Task<ApiResponse<object>> DeleteActivityGroupAsync(Guid id, bool isActive, Employee loggedInEmployee, Guid tenantId); Task<ApiResponse<object>> DeleteActivityGroupAsync(Guid id, bool isActive, Employee loggedInEmployee, Guid tenantId);
#endregion #endregion
#region =================================================================== Contact Category APIs =================================================================== #region =================================================================== Contact Category APIs ===================================================================
Task<ApiResponse<object>> CreateContactCategory(CreateContactCategoryDto contactCategoryDto, Employee loggedInEmployee, Guid tenantId); Task<ApiResponse<object>> CreateContactCategory(CreateContactCategoryDto contactCategoryDto, Employee loggedInEmployee, Guid tenantId);
Task<ApiResponse<object>> UpdateContactCategory(Guid id, UpdateContactCategoryDto contactCategoryDto, Employee loggedInEmployee, Guid tenantId); Task<ApiResponse<object>> UpdateContactCategory(Guid id, UpdateContactCategoryDto contactCategoryDto, Employee loggedInEmployee, Guid tenantId);
@ -104,5 +106,12 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces
Task<ApiResponse<object>> UpdateDocumentTypeMasterAsync(Guid id, CreateDocumentTypeDto model, Employee loggedInEmployee, Guid tenantId); Task<ApiResponse<object>> UpdateDocumentTypeMasterAsync(Guid id, CreateDocumentTypeDto model, Employee loggedInEmployee, Guid tenantId);
Task<ApiResponse<object>> DeleteDocumentTypeMasterAsync(Guid id, bool isActive, Employee loggedInEmployee, Guid tenantId); Task<ApiResponse<object>> DeleteDocumentTypeMasterAsync(Guid id, bool isActive, Employee loggedInEmployee, Guid tenantId);
#endregion #endregion
#region =================================================================== Payment Adjustment Head APIs ===================================================================
Task<ApiResponse<object>> GetPaymentAdjustmentHeadListAsync(bool isActive, Employee loggedInEmployee, Guid tenantId);
Task<ApiResponse<object>> CreatePaymentAdjustmentHeadAsync(PaymentAdjustmentHeadDto model, Employee loggedInEmployee, Guid tenantId);
Task<ApiResponse<object>> UpdatePaymentAdjustmentHeadAsync(Guid id, PaymentAdjustmentHeadDto model, Employee loggedInEmployee, Guid tenantId);
Task<ApiResponse<object>> DeletePaymentAdjustmentHeadAsync(Guid id, bool isActive, Employee loggedInEmployee, Guid tenantId);
#endregion
} }
} }

View File

@ -10,7 +10,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces
{ {
public interface IProjectServices public interface IProjectServices
{ {
Task<ApiResponse<object>> GetAllProjectsBasicAsync(Guid tenantId, Employee loggedInEmployee); Task<ApiResponse<object>> GetAllProjectsBasicAsync(bool provideAll, Guid tenantId, Employee loggedInEmployee);
Task<ApiResponse<object>> GetAllProjectsAsync(Guid tenantId, Employee loggedInEmployee); Task<ApiResponse<object>> GetAllProjectsAsync(Guid tenantId, Employee loggedInEmployee);
Task<ApiResponse<object>> GetProjectAsync(Guid id, Guid tenantId, Employee loggedInEmployee); Task<ApiResponse<object>> GetProjectAsync(Guid id, Guid tenantId, Employee loggedInEmployee);
Task<ApiResponse<object>> GetProjectDetailsAsync(Guid id, Guid tenantId, Employee loggedInEmployee); Task<ApiResponse<object>> GetProjectDetailsAsync(Guid id, Guid tenantId, Employee loggedInEmployee);