Added the payment related APIs

This commit is contained in:
ashutosh.nehete 2025-10-29 16:29:39 +05:30
parent 7e634e3a47
commit c3b93022fd
36 changed files with 9469 additions and 54 deletions

View File

@ -11,6 +11,7 @@ using Marco.Pms.Model.Forum;
using Marco.Pms.Model.Mail;
using Marco.Pms.Model.Master;
using Marco.Pms.Model.OrganizationModel;
using Marco.Pms.Model.PaymentGetway;
using Marco.Pms.Model.Projects;
using Marco.Pms.Model.Roles;
using Marco.Pms.Model.TenantModels;
@ -141,6 +142,9 @@ namespace Marco.Pms.DataAccess.Data
public DbSet<ReceivedInvoicePayment> ReceivedInvoicePayments { get; set; }
public DbSet<PaymentAdjustmentHead> PaymentAdjustmentHeads { get; set; }
public DbSet<PaymentDetail> PaymentDetails { get; set; }
public DbSet<TenantEnquire> TenantEnquires { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,469 @@
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_Payment_Related_Tables : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<Guid>(
name: "PaymentDetailId",
table: "TenantSubscriptions",
type: "char(36)",
nullable: true,
collation: "ascii_general_ci");
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"),
EInvoiceNumber = table.Column<string>(type: "longtext", nullable: true)
.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),
BasicAmount = table.Column<double>(type: "double", nullable: false),
TaxAmount = table.Column<double>(type: "double", nullable: false),
IsActive = table.Column<bool>(type: "tinyint(1)", nullable: false),
MarkAsCompleted = 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: "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.CreateTable(
name: "PaymentDetails",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
PaymentId = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
OrderId = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Status = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Method = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
EncryptedDetails = table.Column<byte[]>(type: "longblob", nullable: true),
Nonce = table.Column<byte[]>(type: "longblob", nullable: true),
Tag = table.Column<byte[]>(type: "longblob", nullable: true),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_PaymentDetails", x => x.Id);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "TenantEnquires",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
FirstName = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
LastName = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
OrganizationName = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Email = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
ContactNumber = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
BillingAddress = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
OrganizationSize = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
IndustryId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
Reference = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4")
},
constraints: table =>
{
table.PrimaryKey("PK_TenantEnquires", x => x.Id);
table.ForeignKey(
name: "FK_TenantEnquires_Industries_IndustryId",
column: x => x.IndustryId,
principalTable: "Industries",
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),
Comment = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
IsActive = table.Column<bool>(type: "tinyint(1)", nullable: false),
PaymentAdjustmentHeadId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
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_PaymentAdjustmentHeads_PaymentAdjust~",
column: x => x.PaymentAdjustmentHeadId,
principalTable: "PaymentAdjustmentHeads",
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.InsertData(
table: "Features",
columns: new[] { "Id", "Description", "IsActive", "ModuleId", "Name" },
values: new object[] { new Guid("fc586e7d-ed1a-45e5-bb51-9f34af98ec13"), "Collection Management is a feature that enables organizations to track, organize, and manage the status and recovery of receivables or assets efficiently throughout their lifecycle, supporting systematic follow-up and resolution of outstanding accounts.", true, new Guid("bf59fd88-b57a-4d67-bf01-3780f385896b"), "Collection Management" });
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.InsertData(
table: "FeaturePermissions",
columns: new[] { "Id", "Description", "FeatureId", "IsEnabled", "Name" },
values: new object[,]
{
{ new Guid("061d9ccd-85b4-4cb0-be06-2f9f32cebb72"), " Enables entry and processing of payment transactions.", new Guid("fc586e7d-ed1a-45e5-bb51-9f34af98ec13"), true, "Add Payment" },
{ new Guid("455187b4-fef1-41f9-b3d0-025d0b6302c3"), "Ability to modify collection properties, content, and access rights.", new Guid("fc586e7d-ed1a-45e5-bb51-9f34af98ec13"), true, "Edit Collection" },
{ new Guid("b93141fd-dbd3-4051-8f57-bf25d18e3555"), "Authorizes users to create new collections for organizing related resources and managing access", new Guid("fc586e7d-ed1a-45e5-bb51-9f34af98ec13"), true, "Create Collection" },
{ new Guid("c8d7eea5-4033-4aad-9ebe-76de49896830"), "View Collection is a permission that allows users to see and browse assets or items within a collection without making any modifications or edits to its contents.", new Guid("fc586e7d-ed1a-45e5-bb51-9f34af98ec13"), true, "View Collection" },
{ new Guid("dbf17591-09fe-4c93-9e1a-12db8f5cc5de"), "Collection Admin is a permission that grants a user full administrative control over collections, including creating, editing, managing access, and deleting collections within a system.", new Guid("fc586e7d-ed1a-45e5-bb51-9f34af98ec13"), true, "Collection Admin" }
});
migrationBuilder.CreateIndex(
name: "IX_TenantSubscriptions_PaymentDetailId",
table: "TenantSubscriptions",
column: "PaymentDetailId");
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_PaymentAdjustmentHeads_TenantId",
table: "PaymentAdjustmentHeads",
column: "TenantId");
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_PaymentAdjustmentHeadId",
table: "ReceivedInvoicePayments",
column: "PaymentAdjustmentHeadId");
migrationBuilder.CreateIndex(
name: "IX_ReceivedInvoicePayments_TenantId",
table: "ReceivedInvoicePayments",
column: "TenantId");
migrationBuilder.CreateIndex(
name: "IX_TenantEnquires_IndustryId",
table: "TenantEnquires",
column: "IndustryId");
migrationBuilder.AddForeignKey(
name: "FK_TenantSubscriptions_PaymentDetails_PaymentDetailId",
table: "TenantSubscriptions",
column: "PaymentDetailId",
principalTable: "PaymentDetails",
principalColumn: "Id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_TenantSubscriptions_PaymentDetails_PaymentDetailId",
table: "TenantSubscriptions");
migrationBuilder.DropTable(
name: "InvoiceAttachments");
migrationBuilder.DropTable(
name: "InvoiceComments");
migrationBuilder.DropTable(
name: "PaymentDetails");
migrationBuilder.DropTable(
name: "ReceivedInvoicePayments");
migrationBuilder.DropTable(
name: "TenantEnquires");
migrationBuilder.DropTable(
name: "Invoices");
migrationBuilder.DropTable(
name: "PaymentAdjustmentHeads");
migrationBuilder.DropIndex(
name: "IX_TenantSubscriptions_PaymentDetailId",
table: "TenantSubscriptions");
migrationBuilder.DeleteData(
table: "FeaturePermissions",
keyColumn: "Id",
keyValue: new Guid("061d9ccd-85b4-4cb0-be06-2f9f32cebb72"));
migrationBuilder.DeleteData(
table: "FeaturePermissions",
keyColumn: "Id",
keyValue: new Guid("455187b4-fef1-41f9-b3d0-025d0b6302c3"));
migrationBuilder.DeleteData(
table: "FeaturePermissions",
keyColumn: "Id",
keyValue: new Guid("b93141fd-dbd3-4051-8f57-bf25d18e3555"));
migrationBuilder.DeleteData(
table: "FeaturePermissions",
keyColumn: "Id",
keyValue: new Guid("c8d7eea5-4033-4aad-9ebe-76de49896830"));
migrationBuilder.DeleteData(
table: "FeaturePermissions",
keyColumn: "Id",
keyValue: new Guid("dbf17591-09fe-4c93-9e1a-12db8f5cc5de"));
migrationBuilder.DeleteData(
table: "Features",
keyColumn: "Id",
keyValue: new Guid("fc586e7d-ed1a-45e5-bb51-9f34af98ec13"));
migrationBuilder.DropColumn(
name: "PaymentDetailId",
table: "TenantSubscriptions");
}
}
}

View File

@ -369,6 +369,273 @@ namespace Marco.Pms.DataAccess.Migrations
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 =>
{
b.Property<Guid>("Id")
@ -1688,6 +1955,46 @@ namespace Marco.Pms.DataAccess.Migrations
Name = "Manage"
},
new
{
Id = new Guid("dbf17591-09fe-4c93-9e1a-12db8f5cc5de"),
Description = "Collection Admin is a permission that grants a user full administrative control over collections, including creating, editing, managing access, and deleting collections within a system.",
FeatureId = new Guid("fc586e7d-ed1a-45e5-bb51-9f34af98ec13"),
IsEnabled = true,
Name = "Collection Admin"
},
new
{
Id = new Guid("c8d7eea5-4033-4aad-9ebe-76de49896830"),
Description = "View Collection is a permission that allows users to see and browse assets or items within a collection without making any modifications or edits to its contents.",
FeatureId = new Guid("fc586e7d-ed1a-45e5-bb51-9f34af98ec13"),
IsEnabled = true,
Name = "View Collection"
},
new
{
Id = new Guid("b93141fd-dbd3-4051-8f57-bf25d18e3555"),
Description = "Authorizes users to create new collections for organizing related resources and managing access",
FeatureId = new Guid("fc586e7d-ed1a-45e5-bb51-9f34af98ec13"),
IsEnabled = true,
Name = "Create Collection"
},
new
{
Id = new Guid("455187b4-fef1-41f9-b3d0-025d0b6302c3"),
Description = "Ability to modify collection properties, content, and access rights.",
FeatureId = new Guid("fc586e7d-ed1a-45e5-bb51-9f34af98ec13"),
IsEnabled = true,
Name = "Edit Collection"
},
new
{
Id = new Guid("061d9ccd-85b4-4cb0-be06-2f9f32cebb72"),
Description = " Enables entry and processing of payment transactions.",
FeatureId = new Guid("fc586e7d-ed1a-45e5-bb51-9f34af98ec13"),
IsEnabled = true,
Name = "Add Payment"
},
new
{
Id = new Guid("068cb3c1-49c5-4746-9f29-1fce16e820ac"),
Description = "Allow user to create new organization",
@ -2829,6 +3136,14 @@ namespace Marco.Pms.DataAccess.Migrations
Name = "Expense Management"
},
new
{
Id = new Guid("fc586e7d-ed1a-45e5-bb51-9f34af98ec13"),
Description = "Collection Management is a feature that enables organizations to track, organize, and manage the status and recovery of receivables or assets efficiently throughout their lifecycle, supporting systematic follow-up and resolution of outstanding accounts.",
IsActive = true,
ModuleId = new Guid("bf59fd88-b57a-4d67-bf01-3780f385896b"),
Name = "Collection Management"
},
new
{
Id = new Guid("9d4b5489-2079-40b9-bd77-6e1bf90bc19f"),
Description = "Manage Tasks",
@ -3809,6 +4124,45 @@ namespace Marco.Pms.DataAccess.Migrations
b.ToTable("TenantOrgMappings");
});
modelBuilder.Entity("Marco.Pms.Model.PaymentGetway.PaymentDetail", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<byte[]>("EncryptedDetails")
.HasColumnType("longblob");
b.Property<string>("Method")
.IsRequired()
.HasColumnType("longtext");
b.Property<byte[]>("Nonce")
.HasColumnType("longblob");
b.Property<string>("OrderId")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("PaymentId")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Status")
.IsRequired()
.HasColumnType("longtext");
b.Property<byte[]>("Tag")
.HasColumnType("longblob");
b.HasKey("Id");
b.ToTable("PaymentDetails");
});
modelBuilder.Entity("Marco.Pms.Model.Projects.Building", b =>
{
b.Property<Guid>("Id")
@ -4268,6 +4622,54 @@ namespace Marco.Pms.DataAccess.Migrations
});
});
modelBuilder.Entity("Marco.Pms.Model.TenantModels.TenantEnquire", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<string>("BillingAddress")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("ContactNumber")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Email")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("FirstName")
.IsRequired()
.HasColumnType("longtext");
b.Property<Guid>("IndustryId")
.HasColumnType("char(36)");
b.Property<string>("LastName")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("OrganizationName")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("OrganizationSize")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Reference")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.HasIndex("IndustryId");
b.ToTable("TenantEnquires");
});
modelBuilder.Entity("Marco.Pms.Model.TenantModels.TenantSubscriptions", b =>
{
b.Property<Guid>("Id")
@ -4304,6 +4706,9 @@ namespace Marco.Pms.DataAccess.Migrations
b.Property<DateTime>("NextBillingDate")
.HasColumnType("datetime(6)");
b.Property<Guid?>("PaymentDetailId")
.HasColumnType("char(36)");
b.Property<Guid>("PlanId")
.HasColumnType("char(36)");
@ -4328,6 +4733,8 @@ namespace Marco.Pms.DataAccess.Migrations
b.HasIndex("CurrencyId");
b.HasIndex("PaymentDetailId");
b.HasIndex("PlanId");
b.HasIndex("StatusId");
@ -4794,6 +5201,139 @@ namespace Marco.Pms.DataAccess.Migrations
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 =>
{
b.HasOne("Marco.Pms.Model.Employees.Employee", "CreatedBy")
@ -6097,6 +6637,17 @@ namespace Marco.Pms.DataAccess.Migrations
b.Navigation("TenantStatus");
});
modelBuilder.Entity("Marco.Pms.Model.TenantModels.TenantEnquire", b =>
{
b.HasOne("Marco.Pms.Model.Master.Industry", "Industry")
.WithMany()
.HasForeignKey("IndustryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Industry");
});
modelBuilder.Entity("Marco.Pms.Model.TenantModels.TenantSubscriptions", b =>
{
b.HasOne("Marco.Pms.Model.Employees.Employee", "CreatedBy")
@ -6111,6 +6662,10 @@ namespace Marco.Pms.DataAccess.Migrations
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Marco.Pms.Model.PaymentGetway.PaymentDetail", "PaymentDetail")
.WithMany()
.HasForeignKey("PaymentDetailId");
b.HasOne("Marco.Pms.Model.TenantModels.SubscriptionPlanDetails", "Plan")
.WithMany()
.HasForeignKey("PlanId")
@ -6137,6 +6692,8 @@ namespace Marco.Pms.DataAccess.Migrations
b.Navigation("Currency");
b.Navigation("PaymentDetail");
b.Navigation("Plan");
b.Navigation("Status");

View File

@ -234,6 +234,27 @@ namespace Marco.Pms.Helpers.CacheHelper
return false;
}
}
public async Task<bool> ClearAllEmployeesFromCacheByTenantId(Guid tenantId)
{
var tenantIdString = tenantId.ToString();
try
{
var filter = Builders<EmployeePermissionMongoDB>.Filter.Eq(e => e.TenantId, tenantIdString);
var result = await _collection.DeleteManyAsync(filter);
if (result.DeletedCount == 0)
return false;
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occured while deleting employee profile");
return false;
}
}
// A private method to handle the one-time setup of the collection's indexes.
private async Task InitializeCollectionAsync()

View File

@ -0,0 +1,7 @@
namespace Marco.Pms.Model.Dtos.PaymentGetway
{
public class CreateOrderDto
{
public double Amount { get; set; }
}
}

View File

@ -0,0 +1,11 @@
namespace Marco.Pms.Model.Dtos.PaymentGetway
{
public class PaymentVerificationRequest
{
public required Guid TenantEnquireId { get; set; }
public required Guid PlanId { get; set; }
public required string OrderId { get; set; }
public required string PaymentId { get; set; }
public required string Signature { get; set; }
}
}

View File

@ -0,0 +1,9 @@
namespace Marco.Pms.Model.Dtos.Tenant
{
public class SelfSubscriptionDto
{
public Guid TenantEnquireId { get; set; }
public Guid PaymentDetailId { get; set; }
public Guid PlanId { get; set; }
}
}

View File

@ -0,0 +1,15 @@
namespace Marco.Pms.Model.Dtos.Tenant
{
public class TenantEnquireDto
{
public required string FirstName { get; set; }
public required string LastName { get; set; }
public required string OrganizationName { get; set; }
public required string Email { get; set; }
public required string ContactNumber { get; set; }
public required string BillingAddress { get; set; }
public required string OrganizationSize { get; set; }
public required Guid IndustryId { get; set; }
public required string Reference { get; set; }
}
}

View File

@ -0,0 +1,16 @@
namespace Marco.Pms.Model.PaymentGetway
{
public class PaymentDetail
{
public Guid Id { get; set; }
public string PaymentId { get; set; } = string.Empty;
public string OrderId { get; set; } = string.Empty;
public string Status { get; set; } = string.Empty; // created, authorized, captured, refunded, failed
public string Method { get; set; } = string.Empty;
public byte[]? EncryptedDetails { get; set; }
public byte[]? Nonce { get; set; }
public byte[]? Tag { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}
}

View File

@ -0,0 +1,24 @@
using Marco.Pms.Model.Master;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using System.ComponentModel.DataAnnotations.Schema;
namespace Marco.Pms.Model.TenantModels
{
public class TenantEnquire
{
public Guid Id { get; set; }
public string FirstName { get; set; } = default!;
public string LastName { get; set; } = default!;
public string OrganizationName { get; set; } = default!;
public string Email { get; set; } = default!;
public string ContactNumber { get; set; } = default!;
public string BillingAddress { get; set; } = default!;
public string OrganizationSize { get; set; } = default!;
public Guid IndustryId { get; set; }
[ForeignKey("IndustryId")]
[ValidateNever]
public Industry? Industry { get; set; }
public string Reference { get; set; } = default!;
}
}

View File

@ -1,5 +1,6 @@
using Marco.Pms.Model.Employees;
using Marco.Pms.Model.Master;
using Marco.Pms.Model.PaymentGetway;
using Marco.Pms.Model.Utilities;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using System.ComponentModel.DataAnnotations.Schema;
@ -28,6 +29,11 @@ namespace Marco.Pms.Model.TenantModels
[ForeignKey("CurrencyId")]
[ValidateNever]
public CurrencyMaster? Currency { get; set; }
public Guid? PaymentDetailId { get; set; }
[ForeignKey("PaymentDetailId")]
[ValidateNever]
public PaymentDetail? PaymentDetail { get; set; }
public DateTime NextBillingDate { get; set; }
public DateTime? CancellationDate { get; set; }
public bool AutoRenew { get; set; } = true;

View File

@ -0,0 +1,8 @@
namespace Marco.Pms.Model.ViewModels.PaymentGetway
{
public class BankDetails
{
public string? Bank { get; set; }
public string? BankCode { get; set; }
}
}

View File

@ -0,0 +1,14 @@
namespace Marco.Pms.Model.ViewModels.PaymentGetway
{
public class CardDetails
{
public string? CardId { get; set; }
public string? Last4Digits { get; set; }
public string? Network { get; set; } // Visa, MasterCard, Amex, RuPay
public string? CardType { get; set; } // credit, debit, prepaid
public string? Issuer { get; set; } // Bank name
public bool International { get; set; }
public bool Emi { get; set; }
public string? SubType { get; set; } // consumer, business
}
}

View File

@ -0,0 +1,8 @@
namespace Marco.Pms.Model.ViewModels.PaymentGetway
{
public class CreateOrderVM
{
public string? OrderId { get; set; }
public string? Key { get; set; }
}
}

View File

@ -0,0 +1,9 @@
namespace Marco.Pms.Model.ViewModels.PaymentGetway
{
public class PaymentDetailsVM
{
public Guid Id { get; set; }
public RazorpayPaymentDetails? RazorpayPaymentDetails { get; set; }
public RazorpayOrderDetails? RazorpayOrderDetails { get; set; }
}
}

View File

@ -0,0 +1,15 @@
namespace Marco.Pms.Model.ViewModels.PaymentGetway
{
public class RazorpayOrderDetails
{
public string? OrderId { get; set; }
public decimal Amount { get; set; }
public string? Currency { get; set; }
public string? Status { get; set; }
public string? Receipt { get; set; }
public DateTime CreatedAt { get; set; }
public decimal AmountPaid { get; set; }
public decimal AmountDue { get; set; }
public int Attempts { get; set; }
}
}

View File

@ -0,0 +1,35 @@
namespace Marco.Pms.Model.ViewModels.PaymentGetway
{
public class RazorpayPaymentDetails
{
public string? PaymentId { get; set; }
public string? OrderId { get; set; }
public decimal Amount { get; set; }
public string? Currency { get; set; }
public string? Status { get; set; } // created, authorized, captured, refunded, failed
public string? Method { get; set; } // card, netbanking, wallet, upi
public string? Email { get; set; }
public string? Contact { get; set; }
public string? Description { get; set; }
public string? CustomerName { get; set; }
public DateTime CreatedAt { get; set; }
// Payment method specific details
public CardDetails? CardDetails { get; set; }
public BankDetails? BankDetails { get; set; }
public UpiDetails? UpiDetails { get; set; }
public WalletDetails? WalletDetails { get; set; }
// Fee and tax
public decimal Fee { get; set; }
public decimal Tax { get; set; }
// Error details (if payment failed)
public string? ErrorCode { get; set; }
public string? ErrorDescription { get; set; }
// Additional flags
public bool InternationalPayment { get; set; }
public bool Captured { get; set; }
}
}

View File

@ -0,0 +1,8 @@
namespace Marco.Pms.Model.ViewModels.PaymentGetway
{
public class UpiDetails
{
public string? Vpa { get; set; } // UPI ID
public string? Provider { get; set; }
}
}

View File

@ -0,0 +1,7 @@
namespace Marco.Pms.Model.ViewModels.PaymentGetway
{
public class WalletDetails
{
public string? WalletName { get; set; } // paytm, phonepe, amazonpay, freecharge, jiomoney, olamoney
}
}

View File

@ -1,6 +1,7 @@
using Marco.Pms.Model.Master;
using Marco.Pms.Model.TenantModels;
using Marco.Pms.Model.ViewModels.Activities;
using Marco.Pms.Model.ViewModels.PaymentGetway;
namespace Marco.Pms.Model.ViewModels.Tenant
{
@ -16,6 +17,7 @@ namespace Marco.Pms.Model.ViewModels.Tenant
public DateTime EndDate { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
public PaymentDetailsVM? PaymentDetail { get; set; }
public BasicEmployeeVM? CreatedBy { get; set; }
public BasicEmployeeVM? updatedBy { get; set; }
public CurrencyMaster? Currency { get; set; }

View File

@ -621,19 +621,19 @@ namespace Marco.Pms.Services.Controllers
var featureIds = await generalHelper.GetFeatureIdsByTenentIdAsync(tenantId);
_logger.LogInfo("Enabled features for TenantId: {TenantId} -> {FeatureIds}", tenantId, string.Join(",", featureIds));
if (!(featureIds?.Any() ?? false))
{
featureIds = new List<Guid>
{
new Guid("a4e25142-449b-4334-a6e5-22f70e4732d7"), // Expense Management feature
new Guid("81ab8a87-8ccd-4015-a917-0627cee6a100"), // Employee Management feature
new Guid("52c9cf54-1eb2-44d2-81bb-524cf29c0a94"), // Attendance Management feature
new Guid("a8cf4331-8f04-4961-8360-a3f7c3cc7462"), // Document Management feature
new Guid("be3b3afc-6ccf-4566-b9b6-aafcb65546be"), // Masters Management feature
new Guid("39e66f81-efc6-446c-95bd-46bff6cfb606"), // Directory Management feature
new Guid("6d4c82d6-dbce-48ab-b8b8-f785f4d8c914") // Organization Management feature
};
}
//if (!(featureIds?.Any() ?? false))
//{
// featureIds = new List<Guid>
// {
// new Guid("a4e25142-449b-4334-a6e5-22f70e4732d7"), // Expense Management feature
// new Guid("81ab8a87-8ccd-4015-a917-0627cee6a100"), // Employee Management feature
// new Guid("52c9cf54-1eb2-44d2-81bb-524cf29c0a94"), // Attendance Management feature
// new Guid("a8cf4331-8f04-4961-8360-a3f7c3cc7462"), // Document Management feature
// new Guid("be3b3afc-6ccf-4566-b9b6-aafcb65546be"), // Masters Management feature
// new Guid("39e66f81-efc6-446c-95bd-46bff6cfb606"), // Directory Management feature
// new Guid("6d4c82d6-dbce-48ab-b8b8-f785f4d8c914") // Organization Management feature
// };
//}
// Aggregate menus based on enabled features
var response = featureIds

View File

@ -0,0 +1,108 @@
using Marco.Pms.Model.Dtos.PaymentGetway;
using Marco.Pms.Model.Utilities;
using Marco.Pms.Services.Service.ServiceInterfaces;
using MarcoBMS.Services.Helpers;
using MarcoBMS.Services.Service;
using Microsoft.AspNetCore.Mvc;
namespace Marco.Pms.Services.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class PaymentController : ControllerBase
{
private readonly UserHelper _userHelper;
private readonly ILoggingService _logger;
private readonly IRazorpayService _razorpayService;
private readonly Guid tenantId;
private readonly Guid organizaionId;
public PaymentController(UserHelper userHelper, ILoggingService logger, IRazorpayService razorpayService)
{
_userHelper = userHelper;
_logger = logger;
_razorpayService = razorpayService;
tenantId = userHelper.GetTenantId();
organizaionId = userHelper.GetCurrentOrganizationId();
}
[HttpPost("create-order")]
public async Task<IActionResult> CreateOrder([FromBody] CreateOrderDto model)
{
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
try
{
var response = _razorpayService.CreateOrder(model.Amount, loggedInEmployee, tenantId);
return Ok(ApiResponse<object>.SuccessResponse(response, "Payment created successfully", 200));
}
catch (Exception ex)
{
return StatusCode(500, ApiResponse<object>.ErrorResponse("Error occured While creating the payment", new
{
Message = ex.Message,
StackTrace = ex.StackTrace,
Source = ex.Source,
InnerException = new
{
Message = ex.InnerException?.Message,
StackTrace = ex.InnerException?.StackTrace,
Source = ex.InnerException?.Source,
}
}, 500));
}
}
[HttpPost("verify-payment")]
public async Task<IActionResult> VerifyPayment([FromBody] PaymentVerificationRequest request)
{
try
{
_logger.LogInfo("Payment verification started for OrderId: {OrderId}, PaymentId: {PaymentId}",
request.OrderId ?? "", request.PaymentId ?? "");
// Validate request
if (string.IsNullOrEmpty(request.OrderId) || string.IsNullOrEmpty(request.PaymentId) || string.IsNullOrEmpty(request.Signature))
{
_logger.LogWarning("Payment verification failed - Missing required parameters");
return BadRequest(ApiResponse<object>.ErrorResponse("Missing required parameters", 400));
}
// Verify signature
string payload = request.OrderId + "|" + request.PaymentId;
string actualSignature = request.Signature;
string expectedSignature = _razorpayService.GetExpectedSignature(payload);
if (actualSignature == expectedSignature)
{
_logger.LogInfo("Payment signature verified successfully for OrderId: {OrderId}", request.OrderId);
// Fetch complete payment details from Razorpay including card details
var response = await _razorpayService.GetPaymentDetails(request.PaymentId);
_logger.LogInfo("Invoice generated and saved for OrderId: {OrderId}", request.OrderId);
return Ok(ApiResponse<object>.SuccessResponse(response, "Payment verified successfully", 200));
}
else
{
_logger.LogWarning("Payment signature verification failed for OrderId: {OrderId}", request.OrderId);
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid signature - Payment verification failed", "Invalid signature - Payment verification failed", 400));
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error during payment verification for OrderId: {OrderId}", request.OrderId ?? "");
return StatusCode(500, ApiResponse<object>.ErrorResponse("An error occurred during payment verification", "An error occurred during payment verification", 500));
}
}
[HttpGet("get/payment-details/{id}")]
public async Task<IActionResult> GetPaymentDetails(Guid id)
{
var paymentsDetails = await _razorpayService.GetPaymentDetailsFromDataBase(id);
return Ok(ApiResponse<object>.SuccessResponse(paymentsDetails, "Payment fetched Successfully", 200));
}
}
}

View File

@ -16,6 +16,7 @@ using Marco.Pms.Model.ViewModels.Activities;
using Marco.Pms.Model.ViewModels.Tenant;
using Marco.Pms.Services.Helpers;
using Marco.Pms.Services.Service;
using Marco.Pms.Services.Service.ServiceInterfaces;
using MarcoBMS.Services.Helpers;
using MarcoBMS.Services.Service;
using Microsoft.AspNetCore.Authorization;
@ -230,6 +231,8 @@ namespace Marco.Pms.Services.Controllers
{
_logger.LogInfo("GetTenantDetails started for TenantId: {TenantId}", id);
using var scope = _serviceScopeFactory.CreateScope();
// Get currently logged-in employee info
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
if (loggedInEmployee == null)
@ -240,32 +243,27 @@ namespace Marco.Pms.Services.Controllers
// Check permissions using a single service scope to avoid overhead
bool hasManagePermission, hasModifyPermission, hasViewPermission;
using (var scope = _serviceScopeFactory.CreateScope())
var manageTask = Task.Run(async () =>
{
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
return await permissionService.HasPermission(PermissionsMaster.ManageTenants, loggedInEmployee.Id);
});
var modifyTask = Task.Run(async () =>
{
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
return await permissionService.HasPermission(PermissionsMaster.ModifyTenant, loggedInEmployee.Id);
});
var viewTask = Task.Run(async () =>
{
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
return await permissionService.HasPermission(PermissionsMaster.ViewTenant, loggedInEmployee.Id);
});
await Task.WhenAll(manageTask, modifyTask, viewTask);
var manageTask = Task.Run(async () =>
{
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
return await permissionService.HasPermission(PermissionsMaster.ManageTenants, loggedInEmployee.Id);
});
var modifyTask = Task.Run(async () =>
{
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
return await permissionService.HasPermission(PermissionsMaster.ModifyTenant, loggedInEmployee.Id);
});
var viewTask = Task.Run(async () =>
{
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
return await permissionService.HasPermission(PermissionsMaster.ViewTenant, loggedInEmployee.Id);
});
await Task.WhenAll(manageTask, modifyTask, viewTask);
hasManagePermission = manageTask.Result;
hasModifyPermission = modifyTask.Result;
hasViewPermission = viewTask.Result;
}
hasManagePermission = manageTask.Result;
hasModifyPermission = modifyTask.Result;
hasViewPermission = viewTask.Result;
if (!hasManagePermission && !hasModifyPermission && !hasViewPermission)
{
@ -353,6 +351,11 @@ namespace Marco.Pms.Services.Controllers
var plans = plansTask.Result;
var projects = projectsTask.Result;
var _razorpayService = scope.ServiceProvider.GetRequiredService<IRazorpayService>();
var paymentDetailIds = plans.Where(ts => ts.PaymentDetailId.HasValue).Select(ts => ts.PaymentDetailId!.Value).ToList();
var paymentsDetails = await _razorpayService.GetPaymentDetailsListFromDataBase(paymentDetailIds);
// Calculate active/inactive employees count
var activeEmployeesCount = employees.Count(e => e.IsActive);
var inActiveEmployeesCount = employees.Count - activeEmployeesCount;
@ -381,9 +384,16 @@ namespace Marco.Pms.Services.Controllers
response.CreatedBy = createdBy;
response.CurrentPlan = _mapper.Map<SubscriptionPlanDetailsVM>(currentPlan);
response.CurrentPlan.PaymentDetail = paymentsDetails.FirstOrDefault(pd => currentPlan != null && pd.Id == currentPlan.PaymentDetailId);
response.CurrentPlanFeatures = await _featureDetailsHelper.GetFeatureDetails(currentPlan?.Plan?.FeaturesId ?? Guid.Empty);
// Map subscription history plans to DTO
response.SubscriptionHistery = _mapper.Map<List<SubscriptionPlanDetailsVM>>(plans);
response.SubscriptionHistery = plans.Select(ts =>
{
var result = _mapper.Map<SubscriptionPlanDetailsVM>(ts);
result.PaymentDetail = paymentsDetails.FirstOrDefault(pd => ts != null && pd.Id == ts.PaymentDetailId);
return result;
}).ToList();
_logger.LogInfo("Tenant details fetched successfully for TenantId: {TenantId}", tenant.Id);
@ -924,6 +934,58 @@ namespace Marco.Pms.Services.Controllers
return Ok(ApiResponse<object>.SuccessResponse(responseData, successMessage, 200));
}
[AllowAnonymous]
[HttpPost("self/create")]
public async Task<IActionResult> SelfRegistrationTenant([FromBody] TenantEnquireDto model)
{
// Log the start of the registration attempt
_logger.LogInfo("Self-registration request received at {Timestamp}.", DateTime.UtcNow);
try
{
// Create db context asynchronously for optimized resource use
await using var context = await _dbContextFactory.CreateDbContextAsync();
// 2. --- VALIDATION ---
// Check if a user with the same email already exists.
var existingUser = await _userManager.FindByEmailAsync(model.Email);
if (existingUser != null)
{
_logger.LogWarning("Tenant creation failed for email {Email}: an application user with this email already exists.", model.Email);
return StatusCode(409, ApiResponse<object>.ErrorResponse("Tenant cannot be created", "A user with the specified email already exists.", 409));
}
var industry = await context.Industries.FirstOrDefaultAsync(i => i.Id == model.IndustryId);
if (industry == null)
{
_logger.LogWarning("Industry not found while creating the tenant enquire");
return NotFound(ApiResponse<object>.ErrorResponse("Industry not found", "Industry not found", 404));
}
// Map DTO to domain model and assign new Guid
var tenantEnquire = _mapper.Map<TenantEnquire>(model);
tenantEnquire.Id = Guid.NewGuid();
// Add new tenant enquiry to the database
await context.TenantEnquires.AddAsync(tenantEnquire);
await context.SaveChangesAsync();
// Log successful registration
_logger.LogInfo("Tenant enquiry created successfully. ID: {TenantEnquireId}", tenantEnquire.Id);
// Return success response with proper status code and user information
return Ok(ApiResponse<object>.SuccessResponse(tenantEnquire, "Tenant enquiry added successfully.", 201));
}
catch (Exception ex)
{
// Log error with detailed exception information at Error level
_logger.LogError(ex, "Error occurred during self-registration: {Message}", ex.Message);
// Return standardized error response to the client
var errorResponse = ApiResponse<object>.ErrorResponse("Failed to add tenant enquiry, please try again later.", "Failed to add tenant enquiry, please try again later.", 500);
return StatusCode(500, errorResponse);
}
}
#endregion
@ -1138,6 +1200,9 @@ namespace Marco.Pms.Services.Controllers
_logger.LogInfo("Removed {Count} role permission mappings for role {RoleId}", deleteMappings.Count, roleId);
}
var _cache = scope.ServiceProvider.GetRequiredService<CacheUpdateHelper>();
await _cache.ClearAllEmployeesFromCacheByTenantId(tenant.Id);
var _masteData = scope.ServiceProvider.GetRequiredService<MasterDataService>();
if (features.Modules?.ProjectManagement?.Enabled ?? false)
@ -1450,6 +1515,9 @@ namespace Marco.Pms.Services.Controllers
_logger.LogInfo("Permissions revoked: {Count} for Role={RoleId}", mappingsToRemove.Count, rootRoleId);
}
var _cache = scope.ServiceProvider.GetRequiredService<CacheUpdateHelper>();
await _cache.ClearAllEmployeesFromCacheByTenantId(tenant.Id);
var _masteData = scope.ServiceProvider.GetRequiredService<MasterDataService>();
if (features.Modules?.ProjectManagement?.Enabled ?? false)
@ -1534,6 +1602,24 @@ namespace Marco.Pms.Services.Controllers
}
}
[AllowAnonymous]
[HttpPost("self/subscription")]
public async Task<IActionResult> SelfSubscriptionAsync(SelfSubscriptionDto model)
{
try
{
using var scope = _serviceScopeFactory.CreateScope();
var _tenantService = scope.ServiceProvider.GetRequiredService<ITenantService>();
var tenant = await _tenantService.CreateTenantAsync(model.TenantEnquireId, model.PaymentDetailId, model.PlanId);
return Ok(ApiResponse<object>.SuccessResponse(tenant, "Tenant Registration Successfully", 201));
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occured while add self subscription");
return StatusCode(500, ApiResponse<object>.ErrorResponse("Error Occured while self subscription", "Error Occured while self subscription", 500));
}
}
#endregion
#region =================================================================== Subscription Plan APIs ===================================================================

View File

@ -956,6 +956,18 @@ namespace Marco.Pms.Services.Helpers
_logger.LogError(ex, "Error occured while deleting all employees from Cache");
}
}
public async Task ClearAllEmployeesFromCacheByTenantId(Guid tenantId)
{
try
{
var response = await _employeeCache.ClearAllEmployeesFromCacheByTenantId(tenantId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occured while deleting all employees from Cache");
}
}
public async Task ClearAllEmployees()
{
try

View File

@ -253,17 +253,19 @@ namespace Marco.Pms.Services.Helpers
!ts.IsCancelled &&
ts.EndDate.Date >= DateTime.UtcNow.Date); // FIX: Subscription should not be expired
var featureIds = new List<Guid>
{
new Guid("a4e25142-449b-4334-a6e5-22f70e4732d7"), // Expense Management feature
new Guid("81ab8a87-8ccd-4015-a917-0627cee6a100"), // Employee Management feature
new Guid("52c9cf54-1eb2-44d2-81bb-524cf29c0a94"), // Attendance Management feature
new Guid("a8cf4331-8f04-4961-8360-a3f7c3cc7462"), // Document Management feature
new Guid("be3b3afc-6ccf-4566-b9b6-aafcb65546be"), // Masters Management feature
new Guid("39e66f81-efc6-446c-95bd-46bff6cfb606"), // Directory Management feature
new Guid("6d4c82d6-dbce-48ab-b8b8-f785f4d8c914") // Organization Management feature
//new Guid("2f3509b7-160d-410a-b9b6-daadd96c986d") // Tenant Management feature
};
//var featureIds = new List<Guid>
//{
// new Guid("a4e25142-449b-4334-a6e5-22f70e4732d7"), // Expense Management feature
// new Guid("81ab8a87-8ccd-4015-a917-0627cee6a100"), // Employee Management feature
// new Guid("52c9cf54-1eb2-44d2-81bb-524cf29c0a94"), // Attendance Management feature
// new Guid("a8cf4331-8f04-4961-8360-a3f7c3cc7462"), // Document Management feature
// new Guid("be3b3afc-6ccf-4566-b9b6-aafcb65546be"), // Masters Management feature
// new Guid("39e66f81-efc6-446c-95bd-46bff6cfb606"), // Directory Management feature
// new Guid("6d4c82d6-dbce-48ab-b8b8-f785f4d8c914") // Organization Management feature
// //new Guid("2f3509b7-160d-410a-b9b6-daadd96c986d") // Tenant Management feature
//};
var featureIds = new List<Guid> { new Guid("be3b3afc-6ccf-4566-b9b6-aafcb65546be") };
if (tenantSubscription == null)
{
@ -274,9 +276,6 @@ namespace Marco.Pms.Services.Helpers
_logger.LogDebug("Active subscription found for tenant: {TenantId}, PlanId: {PlanId}",
tenantId, tenantSubscription.Plan!.Id);
//var featureIds = new List<Guid> { new Guid("be3b3afc-6ccf-4566-b9b6-aafcb65546be") };
// Step 2: Get feature details from Plan
var featureDetails = await _featureDetailsHelper.GetFeatureDetails(tenantSubscription.Plan!.FeaturesId);

View File

@ -61,6 +61,7 @@ namespace Marco.Pms.Services.MappingProfiles
#endregion
#region ======================================================= Tenant =======================================================
CreateMap<TenantEnquireDto, TenantEnquire>();
CreateMap<Tenant, TenantVM>();
CreateMap<Tenant, TenantListVM>();
CreateMap<Tenant, TenantDetailsVM>();

View File

@ -34,6 +34,7 @@
<PackageReference Include="Mime-Detective" Version="24.12.2" />
<PackageReference Include="Mime-Detective.Definitions.Exhaustive" Version="24.12.2" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" />
<PackageReference Include="Razorpay" Version="3.3.2" />
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageReference Include="Serilog.Sinks.Async" Version="2.1.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />

View File

@ -1,6 +1,6 @@
using Marco.Pms.CacheHelper;
using FirebaseAdmin;
using Google.Apis.Auth.OAuth2;
using Marco.Pms.CacheHelper;
using Marco.Pms.DataAccess.Data;
using Marco.Pms.Helpers;
using Marco.Pms.Helpers.CacheHelper;
@ -55,7 +55,7 @@ builder.Services.AddCors(options =>
policy.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader()
.WithExposedHeaders("Authorization");
.WithExposedHeaders("Authorization", "X-Request-ID", "X-Correlation-ID");
});
// A stricter policy for production (loaded from config)
@ -65,7 +65,8 @@ builder.Services.AddCors(options =>
{
policy.WithOrigins(allowedOrigins)
.AllowAnyMethod()
.AllowAnyHeader();
.AllowAnyHeader()
.WithExposedHeaders("Authorization", "X-Request-ID", "X-Correlation-ID");
});
});
#endregion
@ -182,6 +183,9 @@ builder.Services.AddScoped<IExpensesService, ExpensesService>();
builder.Services.AddScoped<IMasterService, MasterService>();
builder.Services.AddScoped<IDirectoryService, DirectoryService>();
builder.Services.AddScoped<IFirebaseService, FirebaseService>();
builder.Services.AddScoped<IRazorpayService, RazorpayService>();
builder.Services.AddScoped<IAesEncryption, AesEncryption>();
builder.Services.AddScoped<ITenantService, TenantService>();
#endregion
#region Helpers

View File

@ -0,0 +1,36 @@
using Marco.Pms.Services.Service.ServiceInterfaces;
using System.Security.Cryptography;
using System.Text;
namespace Marco.Pms.Services.Service
{
public class AesEncryption : IAesEncryption
{
public (byte[] ciphertext, byte[] nonce, byte[] tag) Encrypt(string plaintext, byte[] key)
{
byte[] autoKey = new byte[32]; // 32 bytes = 256 bits
RandomNumberGenerator.Fill(autoKey);
var stringKey = Convert.ToBase64String(autoKey);
byte[] nonce = RandomNumberGenerator.GetBytes(12);
byte[] plaintextBytes = Encoding.UTF8.GetBytes(plaintext);
byte[] ciphertext = new byte[plaintextBytes.Length];
byte[] tag = new byte[16];
using var aes = new AesGcm(key, 16);
aes.Encrypt(nonce, plaintextBytes, ciphertext, tag);
return (ciphertext, nonce, tag);
}
public string Decrypt(byte[] ciphertext, byte[] nonce, byte[] tag, byte[] key)
{
byte[] plaintext = new byte[ciphertext.Length];
using var aes = new AesGcm(key, 16);
aes.Decrypt(nonce, ciphertext, tag, plaintext);
return Encoding.UTF8.GetString(plaintext);
}
}
}

View File

@ -0,0 +1,418 @@
using Marco.Pms.DataAccess.Data;
using Marco.Pms.Model.Employees;
using Marco.Pms.Model.PaymentGetway;
using Marco.Pms.Model.ViewModels.PaymentGetway;
using Marco.Pms.Services.Service.ServiceInterfaces;
using MarcoBMS.Services.Service;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
using Razorpay.Api;
using System.Security.Cryptography;
using System.Text;
namespace Marco.Pms.Services.Service
{
public class RazorpayService : IRazorpayService
{
private readonly ApplicationDbContext _context;
private readonly RazorpayClient _razorpayClient;
private readonly ILoggingService _logger;
private readonly IConfiguration _configuration;
private readonly IAesEncryption _aesEncryption;
private readonly string key;
private readonly string secret;
private readonly byte[] encryptionKey;
public RazorpayService(ApplicationDbContext context, IConfiguration configuration, ILoggingService logger, IAesEncryption aesEncryption)
{
_context = context;
_logger = logger;
_configuration = configuration;
_aesEncryption = aesEncryption;
key = configuration["Razorpay:Key"] ?? "";
secret = configuration["Razorpay:Secret"] ?? "";
_razorpayClient = new RazorpayClient(key, secret);
string stringKey = configuration["Encryption:PaymentKey"] ?? "";
encryptionKey = Convert.FromBase64String(stringKey);
}
public CreateOrderVM CreateOrder(double amount, Employee loggedInEmployee, Guid tenantId)
{
RazorpayClient client = new RazorpayClient(key, secret);
var receipt = $"rec_{Guid.NewGuid()}";
var length = receipt.Length;
Dictionary<string, object> options = new Dictionary<string, object>
{
{ "amount", amount * 100 }, // amount in paise
{ "currency", "INR" },
{ "receipt", receipt},
{ "payment_capture", 1 }
};
Order order = client.Order.Create(options);
var response = new CreateOrderVM
{
OrderId = order["id"],
Key = key
};
return response;
}
public string GetExpectedSignature(string payload)
{
string expectedSignature;
using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret)))
{
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(payload));
expectedSignature = Convert.ToHexString(hash).ToLower();
}
return expectedSignature;
}
/// <summary>
/// Fetch complete payment details from Razorpay including card details
/// </summary>
/// <param name="paymentId">Razorpay Payment ID</param>
/// <returns>Complete payment details with card information</returns>
public async Task<PaymentDetailsVM> GetPaymentDetails(string paymentId)
{
try
{
_logger.LogInfo("Fetching payment details from Razorpay for PaymentId: {PaymentId}", paymentId);
// Fetch payment details from Razorpay
Payment payment = _razorpayClient.Payment.Fetch(paymentId);
// Extract customer name from notes or fetch from customer API
string customerName = ExtractCustomerName(payment);
Guid paymentDetailsId = Guid.NewGuid();
// Map to custom model with all details
var paymentDetails = new RazorpayPaymentDetails
{
PaymentId = payment.Attributes["id"]?.ToString(),
OrderId = payment.Attributes["order_id"]?.ToString(),
Amount = Convert.ToDecimal(payment.Attributes["amount"]) / 100, // Convert from paise to rupees
Currency = payment.Attributes["currency"]?.ToString(),
Status = payment.Attributes["status"]?.ToString(),
Method = payment.Attributes["method"]?.ToString(),
Email = payment.Attributes["email"]?.ToString(),
Contact = payment.Attributes["contact"]?.ToString(),
Description = payment.Attributes["description"]?.ToString(),
CustomerName = customerName,
CreatedAt = DateTimeOffset.FromUnixTimeSeconds(Convert.ToInt64(payment.Attributes["created_at"])).DateTime,
// Card details (if payment method is card)
CardDetails = payment.Attributes["method"]?.ToString() == "card"
? ExtractCardDetails(payment)
: null,
// Bank details (if payment method is netbanking)
BankDetails = payment.Attributes["method"]?.ToString() == "netbanking"
? ExtractBankDetails(payment)
: null,
// UPI details (if payment method is upi)
UpiDetails = payment.Attributes["method"]?.ToString() == "upi"
? ExtractUpiDetails(payment)
: null,
// Wallet details (if payment method is wallet)
WalletDetails = payment.Attributes["method"]?.ToString() == "wallet"
? ExtractWalletDetails(payment)
: null,
// Additional details
Fee = payment.Attributes["fee"] != null
? Convert.ToDecimal(payment.Attributes["fee"]) / 100
: 0,
Tax = payment.Attributes["tax"] != null
? Convert.ToDecimal(payment.Attributes["tax"]) / 100
: 0,
ErrorCode = payment.Attributes["error_code"]?.ToString(),
ErrorDescription = payment.Attributes["error_description"]?.ToString(),
InternationalPayment = Convert.ToBoolean(payment.Attributes["international"] ?? false),
Captured = Convert.ToBoolean(payment.Attributes["captured"] ?? false)
};
var razorpayOrderDetails = GetOrderDetails(paymentDetails.OrderId ?? "");
var response = new PaymentDetailsVM
{
Id = paymentDetailsId,
RazorpayPaymentDetails = paymentDetails,
RazorpayOrderDetails = razorpayOrderDetails
};
string jsonString = JsonConvert.SerializeObject(response);
var data = _aesEncryption.Encrypt(jsonString, encryptionKey);
var paymentDetail = new PaymentDetail
{
Id = paymentDetailsId,
PaymentId = paymentDetails.PaymentId ?? "",
OrderId = paymentDetails.OrderId ?? "",
Status = paymentDetails.Status ?? "",
Method = paymentDetails.Method ?? "",
CreatedAt = DateTime.UtcNow,
EncryptedDetails = data.ciphertext,
Nonce = data.nonce,
Tag = data.tag
};
_context.PaymentDetails.Add(paymentDetail);
await _context.SaveChangesAsync();
_logger.LogInfo("Payment details fetched successfully from Razorpay for PaymentId: {PaymentId}", paymentId);
return response;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error fetching payment details from Razorpay for PaymentId: {PaymentId}", paymentId);
return new PaymentDetailsVM();
}
}
/// <summary>
/// Fetch order details from Razorpay
/// </summary>
/// <param name="orderId">Razorpay Order ID</param>
/// <returns>Order details</returns>
public RazorpayOrderDetails? GetOrderDetails(string orderId)
{
try
{
_logger.LogInfo("Fetching order details from Razorpay for OrderId: {OrderId}", orderId);
Order order = _razorpayClient.Order.Fetch(orderId);
var orderDetails = new RazorpayOrderDetails
{
OrderId = order.Attributes["id"]?.ToString(),
Amount = Convert.ToDecimal(order.Attributes["amount"]) / 100,
Currency = order.Attributes["currency"]?.ToString(),
Status = order.Attributes["status"]?.ToString(),
Receipt = order.Attributes["receipt"]?.ToString(),
CreatedAt = DateTimeOffset.FromUnixTimeSeconds(Convert.ToInt64(order.Attributes["created_at"])).DateTime,
AmountPaid = order.Attributes["amount_paid"] != null
? Convert.ToDecimal(order.Attributes["amount_paid"]) / 100
: 0,
AmountDue = order.Attributes["amount_due"] != null
? Convert.ToDecimal(order.Attributes["amount_due"]) / 100
: 0,
Attempts = Convert.ToInt32(order.Attributes["attempts"] ?? 0)
};
_logger.LogInfo("Order details fetched successfully from Razorpay for OrderId: {OrderId}", orderId);
return orderDetails;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error fetching order details from Razorpay for OrderId: {OrderId}", orderId);
return null;
}
}
public async Task<PaymentDetailsVM> GetPaymentDetailsFromDataBase(Guid id)
{
var projectDetails = await _context.PaymentDetails.FirstOrDefaultAsync(pd => pd.Id == id);
if (projectDetails == null)
{
return new PaymentDetailsVM();
}
string decrypedData = _aesEncryption.Decrypt(projectDetails.EncryptedDetails ?? new byte[32], projectDetails.Nonce ?? new byte[12], projectDetails.Tag ?? new byte[16], encryptionKey);
// Deserialize JSON string to a Department object
PaymentDetailsVM? vm = JsonConvert.DeserializeObject<PaymentDetailsVM>(decrypedData);
if (vm == null)
{
return new PaymentDetailsVM();
}
else
{
return vm;
}
}
public async Task<List<PaymentDetailsVM>> GetPaymentDetailsListFromDataBase(List<Guid> paymentDetailsIds)
{
var projectDetails = await _context.PaymentDetails.Where(pd => paymentDetailsIds.Contains(pd.Id)).ToListAsync();
List<PaymentDetailsVM> response = new List<PaymentDetailsVM>();
foreach (var projectDetail in projectDetails)
{
string decrypedData = _aesEncryption.Decrypt(projectDetail.EncryptedDetails ?? new byte[32], projectDetail.Nonce ?? new byte[12], projectDetail.Tag ?? new byte[16], encryptionKey);
// Deserialize JSON string to a Department object
PaymentDetailsVM? vm = JsonConvert.DeserializeObject<PaymentDetailsVM>(decrypedData);
if (vm != null)
{
response.Add(vm);
}
}
return response;
}
/// <summary>
/// Extract card details from payment
/// </summary>
private CardDetails? ExtractCardDetails(Payment payment)
{
try
{
var cardObj = payment.Attributes["card"];
if (cardObj == null) return null;
string json = JsonConvert.SerializeObject(cardObj);
Dictionary<string, object>? cardDict = JsonConvert.DeserializeObject<Dictionary<string, object>>(json);
return new CardDetails
{
CardId = cardDict?["id"]?.ToString(),
Last4Digits = cardDict?["last4"]?.ToString(),
Network = cardDict?["network"]?.ToString(), // Visa, MasterCard, Amex, etc.
CardType = cardDict?["type"]?.ToString(), // credit, debit, prepaid
Issuer = cardDict?["issuer"]?.ToString(), // Bank name
International = Convert.ToBoolean(cardDict?["international"] ?? false),
Emi = Convert.ToBoolean(cardDict?["emi"] ?? false),
SubType = cardDict?["sub_type"]?.ToString() // consumer, business
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Error extracting card details");
return null;
}
}
/// <summary>
/// Extract bank details from payment
/// </summary>
private BankDetails? ExtractBankDetails(Payment payment)
{
try
{
return new BankDetails
{
Bank = payment.Attributes["bank"]?.ToString(),
BankCode = payment.Attributes["bank_code"]?.ToString()
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Error extracting bank details");
return null;
}
}
/// <summary>
/// Extract UPI details from payment
/// </summary>
private UpiDetails? ExtractUpiDetails(Payment payment)
{
try
{
var vpaObj = payment.Attributes["vpa"];
return new UpiDetails
{
Vpa = vpaObj?.ToString(), // UPI ID
Provider = payment.Attributes["provider"]?.ToString()
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Error extracting UPI details");
return null;
}
}
/// <summary>
/// Extract wallet details from payment
/// </summary>
private WalletDetails? ExtractWalletDetails(Payment payment)
{
try
{
return new WalletDetails
{
WalletName = payment.Attributes["wallet"]?.ToString() // paytm, phonepe, amazonpay, etc.
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Error extracting wallet details");
return null;
}
}
/// <summary>
/// Extract customer name from payment object
/// </summary>
private string ExtractCustomerName(Payment payment)
{
try
{
// Option 1: Get from notes (if you passed customer name during order creation)
var notesObj = payment.Attributes["notes"];
if (notesObj != null)
{
var notesDict = notesObj as Dictionary<string, object>;
if (notesDict != null && notesDict.ContainsKey("customer_name"))
{
return notesDict["customer_name"]?.ToString() ?? "CustomerName";
}
if (notesDict != null && notesDict.ContainsKey("name"))
{
return notesDict["name"]?.ToString() ?? "CustomerName";
}
}
// Option 2: Get from customer ID and fetch customer details
var customerId = payment.Attributes["customer_id"]?.ToString();
if (!string.IsNullOrEmpty(customerId))
{
try
{
Customer customer = _razorpayClient.Customer.Fetch(customerId);
return customer.Attributes["name"]?.ToString() ?? "CustomerName";
}
catch (Exception ex)
{
_logger.LogError(ex, "Error fetching customer details for CustomerId: {CustomerId}", customerId);
}
}
// Option 3: Extract from card holder name (if available)
if (payment.Attributes["method"]?.ToString() == "card")
{
var cardObj = payment.Attributes["card"];
if (cardObj != null)
{
string json = JsonConvert.SerializeObject(cardObj);
Dictionary<string, object>? dictionary = JsonConvert.DeserializeObject<Dictionary<string, object>>(json);
var cardName = dictionary?["name"]?.ToString();
if (!string.IsNullOrEmpty(cardName))
{
return cardName;
}
}
}
// Option 4: Use email as fallback
return payment.Attributes["email"]?.ToString() ?? "Customer";
}
catch (Exception ex)
{
_logger.LogError(ex, "Error extracting customer name from payment");
return "Customer";
}
}
}
}

View File

@ -0,0 +1,8 @@
namespace Marco.Pms.Services.Service.ServiceInterfaces
{
public interface IAesEncryption
{
(byte[] ciphertext, byte[] nonce, byte[] tag) Encrypt(string plaintext, byte[] key);
string Decrypt(byte[] ciphertext, byte[] nonce, byte[] tag, byte[] key);
}
}

View File

@ -0,0 +1,15 @@
using Marco.Pms.Model.Employees;
using Marco.Pms.Model.ViewModels.PaymentGetway;
namespace Marco.Pms.Services.Service.ServiceInterfaces
{
public interface IRazorpayService
{
CreateOrderVM CreateOrder(double amount, Employee loggedInEmployee, Guid tenantId);
string GetExpectedSignature(string payload);
Task<PaymentDetailsVM> GetPaymentDetails(string paymentId);
Task<PaymentDetailsVM> GetPaymentDetailsFromDataBase(Guid id);
Task<List<PaymentDetailsVM>> GetPaymentDetailsListFromDataBase(List<Guid> paymentDetailsIds);
RazorpayOrderDetails? GetOrderDetails(string orderId);
}
}

View File

@ -0,0 +1,9 @@
using Marco.Pms.Model.Utilities;
namespace Marco.Pms.Services.Service.ServiceInterfaces
{
public interface ITenantService
{
Task<ApiResponse<object>> CreateTenantAsync(Guid enquireId, Guid paymentDetailId, Guid planId);
}
}

View File

@ -0,0 +1,682 @@
using AutoMapper;
using Marco.Pms.DataAccess.Data;
using Marco.Pms.Helpers.Utility;
using Marco.Pms.Model.Dtos.Tenant;
using Marco.Pms.Model.Employees;
using Marco.Pms.Model.Entitlements;
using Marco.Pms.Model.OrganizationModel;
using Marco.Pms.Model.Projects;
using Marco.Pms.Model.Roles;
using Marco.Pms.Model.TenantModels;
using Marco.Pms.Model.TenantModels.MongoDBModel;
using Marco.Pms.Model.Utilities;
using Marco.Pms.Model.ViewModels.Activities;
using Marco.Pms.Model.ViewModels.Tenant;
using Marco.Pms.Services.Helpers;
using Marco.Pms.Services.Service.ServiceInterfaces;
using MarcoBMS.Services.Helpers;
using MarcoBMS.Services.Service;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using System.Net;
namespace Marco.Pms.Services.Service
{
public class TenantService : ITenantService
{
private readonly IDbContextFactory<ApplicationDbContext> _dbContextFactory;
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly ILoggingService _logger;
private readonly UserManager<ApplicationUser> _userManager;
private readonly IMapper _mapper;
private readonly UserHelper _userHelper;
private readonly FeatureDetailsHelper _featureDetailsHelper;
private readonly static Guid projectActiveStatus = Guid.Parse("b74da4c2-d07e-46f2-9919-e75e49b12731");
private readonly static Guid projectInProgressStatus = Guid.Parse("cdad86aa-8a56-4ff4-b633-9c629057dfef");
private readonly static Guid projectOnHoldStatus = Guid.Parse("603e994b-a27f-4e5d-a251-f3d69b0498ba");
private readonly static Guid projectInActiveStatus = Guid.Parse("ef1c356e-0fe0-42df-a5d3-8daee355492d");
private readonly static Guid projectCompletedStatus = Guid.Parse("33deaef9-9af1-4f2a-b443-681ea0d04f81");
private readonly static Guid tenantActiveStatus = Guid.Parse("62b05792-5115-4f99-8ff5-e8374859b191");
private readonly static Guid activePlanStatus = Guid.Parse("cd3a68ea-41fd-42f0-bd0c-c871c7337727");
private readonly static Guid EmployeeFeatureId = Guid.Parse("81ab8a87-8ccd-4015-a917-0627cee6a100");
private readonly static string AdminRoleName = "Admin";
public TenantService(IDbContextFactory<ApplicationDbContext> dbContextFactory,
IServiceScopeFactory serviceScopeFactory,
ILoggingService logger,
UserManager<ApplicationUser> userManager,
IMapper mapper,
UserHelper userHelper,
FeatureDetailsHelper featureDetailsHelper)
{
_dbContextFactory = dbContextFactory ?? throw new ArgumentNullException(nameof(dbContextFactory));
_serviceScopeFactory = serviceScopeFactory ?? throw new ArgumentNullException(nameof(serviceScopeFactory));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_userManager = userManager ?? throw new ArgumentNullException(nameof(userManager));
_mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
_userHelper = userHelper ?? throw new ArgumentNullException(nameof(userHelper));
_featureDetailsHelper = featureDetailsHelper ?? throw new ArgumentNullException(nameof(featureDetailsHelper));
}
public async Task<ApiResponse<object>> CreateTenantAsync(Guid enquireId, Guid paymentDetailId, Guid planId)
{
using var scope = _serviceScopeFactory.CreateScope();
await using var _context = await _dbContextFactory.CreateDbContextAsync();
var _configuration = scope.ServiceProvider.GetRequiredService<IConfiguration>();
var _emailSender = scope.ServiceProvider.GetRequiredService<IEmailSender>();
var tenantEnquireTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.TenantEnquires.FirstOrDefaultAsync(te => te.Id == enquireId);
});
var paymentDetailTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.PaymentDetails.FirstOrDefaultAsync(pd => pd.Id == paymentDetailId);
});
var subscriptionPlanTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.SubscriptionPlanDetails.Include(sp => sp.Plan).FirstOrDefaultAsync(sp => sp.Id == planId);
});
await Task.WhenAll(tenantEnquireTask, paymentDetailTask, subscriptionPlanTask);
var tenantEnquire = tenantEnquireTask.Result;
var paymentDetail = paymentDetailTask.Result;
var subscriptionPlan = subscriptionPlanTask.Result;
if (tenantEnquire == null)
{
_logger.LogWarning("Tenant Enquire {TenantEnquireId} not found in database", enquireId);
return ApiResponse<object>.ErrorResponse("Tenant Enquire not found", "Tenant Enquire not found", 404);
}
if (paymentDetail == null)
{
_logger.LogWarning("Payment Details {PaymentDetailsId} not found in database", paymentDetailId);
return ApiResponse<object>.ErrorResponse("Payment Details not found", "Payment Details not found", 404);
}
if (subscriptionPlan == null)
{
_logger.LogWarning("Subscription plan {PlanId} not found in database", planId);
return ApiResponse<object>.ErrorResponse("Subscription plan not found", "Subscription plan not found", 404);
}
var existingUser = await _userManager.FindByEmailAsync(tenantEnquire.Email);
if (existingUser != null)
{
_logger.LogWarning("Tenant creation failed for email {Email}: an application user with this email already exists.", tenantEnquire.Email);
return ApiResponse<object>.ErrorResponse("Tenant cannot be created", "A user with the specified email already exists.", 409);
}
await using var transaction = await _context.Database.BeginTransactionAsync();
try
{
Guid employeeId = Guid.NewGuid();
DateTime onBoardingDate = DateTime.UtcNow;
Guid tenantId = Guid.NewGuid();
// Get last SPRID and increment for new organization
var lastOrganization = await _context.Organizations.OrderByDescending(sp => sp.SPRID).FirstOrDefaultAsync();
double lastSPRID = lastOrganization?.SPRID ?? 5400;
// Map DTO to entity and set defaults
Organization organization = new Organization
{
Name = tenantEnquire.OrganizationName,
Email = tenantEnquire.Email,
ContactPerson = $"{tenantEnquire.FirstName} {tenantEnquire.LastName}",
Address = tenantEnquire.BillingAddress,
ContactNumber = tenantEnquire.ContactNumber,
SPRID = lastSPRID + 1,
CreatedAt = DateTime.UtcNow,
CreatedById = employeeId,
IsActive = true
};
_context.Organizations.Add(organization);
// Create the primary Tenant entity
var tenant = new Tenant
{
Id = tenantId,
Name = tenantEnquire.OrganizationName,
Email = tenantEnquire.Email,
ContactName = $"{tenantEnquire.FirstName} {tenantEnquire.LastName}",
ContactNumber = tenantEnquire.ContactNumber,
OrganizationSize = tenantEnquire.OrganizationSize,
BillingAddress = tenantEnquire.BillingAddress,
IndustryId = tenantEnquire.IndustryId,
Reference = tenantEnquire.Reference,
OnBoardingDate = onBoardingDate,
TenantStatusId = tenantActiveStatus,
OrganizationId = organization.Id,
CreatedById = employeeId,
IsActive = true,
IsSuperTenant = false
};
_context.Tenants.Add(tenant);
// Create the root ApplicationUser for the new tenant
var applicationUser = new ApplicationUser
{
Email = tenantEnquire.Email,
UserName = tenantEnquire.Email, // Best practice to use email as username for simplicity
IsRootUser = true,
EmailConfirmed = true // Auto-confirming email as it's part of a trusted setup process
};
// SECURITY WARNING: Hardcoded passwords are a major vulnerability.
// Replace "User@123" with a securely generated random password.
var initialPassword = "User@123"; // TODO: Replace with password generation service.
var result = await _userManager.CreateAsync(applicationUser, initialPassword);
if (!result.Succeeded)
{
// If user creation fails, roll back the transaction immediately and return the errors.
await transaction.RollbackAsync();
var errors = result.Errors.Select(e => e.Description).ToList();
_logger.LogWarning("Failed to create ApplicationUser for tenant {TenantName}. Errors: {Errors}", tenantEnquire.OrganizationName, string.Join(", ", errors));
return ApiResponse<object>.ErrorResponse("Failed to create user", errors, 400);
}
// Create the default "Admin" Job Role for the tenant
var adminJobRole = new JobRole
{
Name = AdminRoleName,
Description = "Default administrator role for the tenant.",
TenantId = tenantId
};
_context.JobRoles.Add(adminJobRole);
// Create the primary Employee record and link it to the ApplicationUser and JobRole
var employeeUser = new Employee
{
Id = employeeId,
FirstName = tenantEnquire.FirstName,
LastName = tenantEnquire.LastName,
Email = tenantEnquire.Email,
PhoneNumber = tenantEnquire.ContactNumber,
JoiningDate = onBoardingDate,
ApplicationUserId = applicationUser.Id,
JobRole = adminJobRole, // Link to the newly created role
CurrentAddress = tenantEnquire.BillingAddress,
IsActive = true,
IsSystem = false,
IsPrimary = true,
OrganizationId = organization.Id,
HasApplicationAccess = true
};
_context.Employees.Add(employeeUser);
var applicationRole = new ApplicationRole
{
Role = "Super User",
Description = "Super User",
IsSystem = true,
TenantId = tenantId
};
_context.ApplicationRoles.Add(applicationRole);
var rolePermissionMappigs = new List<RolePermissionMappings> {
new RolePermissionMappings
{
ApplicationRoleId = applicationRole.Id,
FeaturePermissionId = PermissionsMaster.ModifyTenant
},
new RolePermissionMappings
{
ApplicationRoleId = applicationRole.Id,
FeaturePermissionId = PermissionsMaster.ViewTenant
},
new RolePermissionMappings
{
ApplicationRoleId = applicationRole.Id,
FeaturePermissionId = PermissionsMaster.ManageMasters
},
new RolePermissionMappings
{
ApplicationRoleId = applicationRole.Id,
FeaturePermissionId = PermissionsMaster.ViewMasters
},
new RolePermissionMappings
{
ApplicationRoleId = applicationRole.Id,
FeaturePermissionId = PermissionsMaster.ViewOrganization
},
new RolePermissionMappings
{
ApplicationRoleId = applicationRole.Id,
FeaturePermissionId = PermissionsMaster.AddOrganization
},
new RolePermissionMappings
{
ApplicationRoleId = applicationRole.Id,
FeaturePermissionId = PermissionsMaster.EditOrganization
}
};
_context.RolePermissionMappings.AddRange(rolePermissionMappigs);
_context.EmployeeRoleMappings.Add(new EmployeeRoleMapping
{
EmployeeId = employeeUser.Id,
RoleId = applicationRole.Id,
IsEnabled = true,
TenantId = tenantId
});
// Create a default project for the new tenant
var project = new Project
{
Name = "Default Project",
ProjectStatusId = Guid.Parse("b74da4c2-d07e-46f2-9919-e75e49b12731"), // Consider using a constant for this GUID
ProjectAddress = tenantEnquire.BillingAddress,
StartDate = onBoardingDate,
EndDate = DateTime.MaxValue,
PromoterId = organization.Id,
PMCId = organization.Id,
ContactPerson = tenant.ContactName,
TenantId = tenantId
};
_context.Projects.Add(project);
var projectAllocation = new ProjectAllocation
{
ProjectId = project.Id,
EmployeeId = employeeUser.Id,
AllocationDate = onBoardingDate,
IsActive = true,
JobRoleId = adminJobRole.Id,
TenantId = tenantId
};
_context.ProjectAllocations.Add(projectAllocation);
// All entities are now added to the context. Save them all in a single database operation.
await _context.SaveChangesAsync();
// 4. --- POST-CREATION ACTIONS ---
// Generate a password reset token so the new user can set their own password.
_logger.LogInfo("User {Email} created. Sending password setup email.", applicationUser.Email);
var token = await _userManager.GeneratePasswordResetTokenAsync(applicationUser);
var resetLink = $"{_configuration["AppSettings:WebFrontendUrl"]}/reset-password?token={WebUtility.UrlEncode(token)}&email={WebUtility.UrlEncode(applicationUser.Email)}";
await _emailSender.SendResetPasswordEmailOnRegister(applicationUser.Email, employeeUser.FirstName, resetLink);
// Map the result to a ViewModel for the API response.
var tenantVM = _mapper.Map<TenantVM>(tenant);
tenantVM.CreatedBy = _mapper.Map<BasicEmployeeVM>(employeeUser);
// Commit the transaction as all operations were successful.
await transaction.CommitAsync();
await AddSubscriptionAsync(tenantId, employeeId, paymentDetailId, planId);
_logger.LogInfo("Successfully created tenant {TenantId} for organization {OrganizationName}.", tenant.Id, tenant.Name);
return ApiResponse<object>.SuccessResponse(tenantVM, "Tenant created successfully.", 201);
}
catch (DbUpdateException dbEx)
{
await transaction.RollbackAsync();
// Log the detailed database exception, including the inner exception if available.
_logger.LogError(dbEx, "A database update exception occurred while creating tenant for email {Email}. Inner Exception: {InnerException}",
tenantEnquire.Email, dbEx.InnerException?.Message ?? string.Empty);
return ApiResponse<object>.ErrorResponse("An internal database error occurred.", ExceptionMapper(dbEx), 500);
}
catch (Exception ex)
{
// Log the general exception.
_logger.LogError(ex, "An unexpected exception occurred while creating tenant for email {Email}.", tenantEnquire.Email);
return ApiResponse<object>.ErrorResponse("An unexpected internal error occurred.", ExceptionMapper(ex), 500);
}
}
public async Task<ApiResponse<object>> AddSubscriptionAsync(Guid tenantId, Guid employeeId, Guid paymentDetailId, Guid planId)
{
_logger.LogInfo("AddSubscription called for Tenant {TenantId} and Plan {PlanId}", tenantId, planId);
await using var _context = await _dbContextFactory.CreateDbContextAsync();
using var scope = _serviceScopeFactory.CreateScope();
var subscriptionPlan = await _context.SubscriptionPlanDetails.Include(sp => sp.Plan).FirstOrDefaultAsync(sp => sp.Id == planId);
var tenant = await _context.Tenants.FirstOrDefaultAsync(t => t.Id == tenantId);
if (tenant == null)
{
_logger.LogWarning("Tenant {TenantId} not found in database", tenantId);
return ApiResponse<object>.ErrorResponse("Tenant not found", "Tenant not found", 404);
}
if (subscriptionPlan == null)
{
_logger.LogWarning("Subscription plan {PlanId} not found in database", planId);
return ApiResponse<object>.ErrorResponse("Subscription plan not found", "Subscription plan not found", 404);
}
var activeUsers = await _context.Employees.CountAsync(e => e.Email != null && e.ApplicationUserId != null && e.TenantId == tenant.Id && e.IsActive);
if (activeUsers > subscriptionPlan.MaxUser)
{
_logger.LogWarning("Add less max user than the active user in the tenant {TenantId}", tenant.Id);
return ApiResponse<object>.ErrorResponse("Invalid Max user count", "Max User count must be higher than active user count", 400);
}
await using var transaction = await _context.Database.BeginTransactionAsync();
var utcNow = DateTime.UtcNow;
// Prepare subscription dates based on frequency
var endDate = subscriptionPlan.Frequency switch
{
PLAN_FREQUENCY.MONTHLY => utcNow.AddDays(30),
PLAN_FREQUENCY.QUARTERLY => utcNow.AddDays(90),
PLAN_FREQUENCY.HALF_YEARLY => utcNow.AddDays(120),
PLAN_FREQUENCY.YEARLY => utcNow.AddDays(360),
_ => utcNow // default if unknown
};
var tenantSubscription = new TenantSubscriptions
{
TenantId = tenantId,
PlanId = planId,
StatusId = activePlanStatus,
CreatedAt = utcNow,
MaxUsers = subscriptionPlan.MaxUser,
CreatedById = employeeId,
CurrencyId = subscriptionPlan.CurrencyId,
PaymentDetailId = paymentDetailId,
IsTrial = false,
StartDate = utcNow,
EndDate = endDate,
NextBillingDate = endDate,
AutoRenew = false
};
_context.TenantSubscriptions.Add(tenantSubscription);
try
{
await _context.SaveChangesAsync();
_logger.LogInfo("Tenant subscription added successfully for Tenant {TenantId}, Plan {PlanId}",
tenantId, planId);
}
catch (DbUpdateException dbEx)
{
_logger.LogError(dbEx, "Database exception while adding subscription plan to tenant {TenantId}", tenantId);
return ApiResponse<object>.ErrorResponse("Internal error occured", ExceptionMapper(dbEx), 500);
}
try
{
var features = await _featureDetailsHelper.GetFeatureDetails(subscriptionPlan.FeaturesId);
if (features == null)
{
_logger.LogInfo("No features found for subscription plan {PlanId}", planId);
await transaction.CommitAsync();
return ApiResponse<object>.SuccessResponse(tenantSubscription, "Tenant subscription successfully added", 200);
}
// Helper to get permissions for a module asynchronously
async Task<List<Guid>> GetPermissionsForModuleAsync(List<Guid>? featureIds)
{
if (featureIds == null || featureIds.Count == 0) return new List<Guid>();
await using var ctx = await _dbContextFactory.CreateDbContextAsync();
return await ctx.FeaturePermissions.AsNoTracking()
.Where(fp => featureIds.Contains(fp.FeatureId))
.Select(fp => fp.Id)
.ToListAsync();
}
// Fetch permission tasks for all modules in parallel
var projectPermissionTask = GetPermissionsForModuleAsync(features.Modules?.ProjectManagement?.FeatureId);
var attendancePermissionTask = GetPermissionsForModuleAsync(features.Modules?.Attendance?.FeatureId);
var directoryPermissionTask = GetPermissionsForModuleAsync(features.Modules?.Directory?.FeatureId);
var expensePermissionTask = GetPermissionsForModuleAsync(features.Modules?.Expense?.FeatureId);
var employeePermissionTask = GetPermissionsForModuleAsync(new List<Guid> { EmployeeFeatureId });
await Task.WhenAll(projectPermissionTask, attendancePermissionTask, directoryPermissionTask, expensePermissionTask, employeePermissionTask);
var newPermissionIds = new List<Guid>();
var deletePermissionIds = new List<Guid>();
// Add or remove permissions based on modules enabled status
void ProcessPermissions(bool? enabled, List<Guid> permissions)
{
if (enabled == true)
newPermissionIds.AddRange(permissions);
else
deletePermissionIds.AddRange(permissions);
}
ProcessPermissions(features.Modules?.ProjectManagement?.Enabled, projectPermissionTask.Result);
ProcessPermissions(features.Modules?.Attendance?.Enabled, attendancePermissionTask.Result);
ProcessPermissions(features.Modules?.Directory?.Enabled, directoryPermissionTask.Result);
ProcessPermissions(features.Modules?.Expense?.Enabled, expensePermissionTask.Result);
newPermissionIds = newPermissionIds.Distinct().ToList();
deletePermissionIds = deletePermissionIds.Distinct().ToList();
// Get root employee and role for this tenant
var rootEmployee = await _context.Employees
.Include(e => e.ApplicationUser)
.FirstOrDefaultAsync(e => e.ApplicationUser != null && (e.ApplicationUser.IsRootUser ?? false) && e.OrganizationId == tenant.OrganizationId);
if (rootEmployee == null)
{
_logger.LogWarning("Root employee not found for tenant {TenantId}", tenantId);
await transaction.CommitAsync();
return ApiResponse<object>.SuccessResponse(tenantSubscription, "Tenant subscription successfully added", 200);
}
var roleId = await _context.EmployeeRoleMappings
.AsNoTracking()
.Where(er => er.EmployeeId == rootEmployee.Id && er.TenantId == tenantId)
.Select(er => er.RoleId)
.FirstOrDefaultAsync();
if (roleId == Guid.Empty)
{
_logger.LogWarning("RoleId for root employee {EmployeeId} in tenant {TenantId} not found", rootEmployee.Id, tenantId);
await transaction.CommitAsync();
return ApiResponse<object>.SuccessResponse(tenantSubscription, "Tenant subscription successfully added", 200);
}
var oldRolePermissionMappings = await _context.RolePermissionMappings
.Where(rp => rp.ApplicationRoleId == roleId)
.ToListAsync();
var oldPermissionIds = oldRolePermissionMappings.Select(rp => rp.FeaturePermissionId).ToList();
// Prevent accidentally deleting essential employee permissions
var permissionIdCount = oldPermissionIds.Count - deletePermissionIds.Count;
if (permissionIdCount <= 4 && deletePermissionIds.Any())
{
var employeePermissionIds = employeePermissionTask.Result;
deletePermissionIds = deletePermissionIds.Where(p => !employeePermissionIds.Contains(p)).ToList();
}
// Prepare mappings to delete and add
var deleteMappings = oldRolePermissionMappings.Where(rp => deletePermissionIds.Contains(rp.FeaturePermissionId)).ToList();
var addRolePermissionMappings = newPermissionIds
.Where(p => !oldPermissionIds.Contains(p))
.Select(p => new RolePermissionMappings
{
ApplicationRoleId = roleId,
FeaturePermissionId = p
})
.ToList();
if (addRolePermissionMappings.Any())
{
_context.RolePermissionMappings.AddRange(addRolePermissionMappings);
_logger.LogInfo("Added {Count} new role permission mappings for role {RoleId}", addRolePermissionMappings.Count, roleId);
}
if (deleteMappings.Any())
{
_context.RolePermissionMappings.RemoveRange(deleteMappings);
_logger.LogInfo("Removed {Count} role permission mappings for role {RoleId}", deleteMappings.Count, roleId);
}
var _cache = scope.ServiceProvider.GetRequiredService<CacheUpdateHelper>();
await _cache.ClearAllEmployeesFromCacheByTenantId(tenant.Id);
var _masteData = scope.ServiceProvider.GetRequiredService<MasterDataService>();
if (features.Modules?.ProjectManagement?.Enabled ?? false)
{
var workCategoryMaster = _masteData.GetWorkCategoriesData(tenant.Id);
var workStatusMaster = _masteData.GetWorkStatusesData(tenant.Id);
_context.WorkCategoryMasters.AddRange(workCategoryMaster);
_context.WorkStatusMasters.AddRange(workStatusMaster);
}
if (features.Modules?.Expense?.Enabled ?? false)
{
var expensesTypeMaster = _masteData.GetExpensesTypeesData(tenant.Id);
var paymentModeMatser = _masteData.GetPaymentModesData(tenant.Id);
_context.ExpensesTypeMaster.AddRange(expensesTypeMaster);
_context.PaymentModeMatser.AddRange(paymentModeMatser);
}
await _context.SaveChangesAsync();
await transaction.CommitAsync();
_logger.LogInfo("Permissions updated successfully for tenant {TenantId} subscription", tenantId);
return ApiResponse<object>.SuccessResponse(tenantSubscription, "Tenant Subscription Successfully", 200);
}
catch (Exception ex)
{
await transaction.RollbackAsync();
_logger.LogError(ex, "Exception occurred while updating permissions for tenant {TenantId}", tenantId);
return ApiResponse<object>.ErrorResponse("Internal error occured", ExceptionMapper(ex), 500);
}
}
#region =================================================================== Helper Functions ===================================================================
private static object ExceptionMapper(Exception ex)
{
return new
{
Message = ex.Message,
StackTrace = ex.StackTrace,
Source = ex.Source,
InnerException = new
{
Message = ex.InnerException?.Message,
StackTrace = ex.InnerException?.StackTrace,
Source = ex.InnerException?.Source,
}
};
}
private bool IsBase64String(string? input)
{
if (string.IsNullOrWhiteSpace(input))
{
return false;
}
string base64Data = input;
const string dataUriMarker = "base64,";
int markerIndex = input.IndexOf(dataUriMarker, StringComparison.Ordinal);
// If the marker is found, extract the actual Base64 data
if (markerIndex >= 0)
{
base64Data = input.Substring(markerIndex + dataUriMarker.Length);
}
// Now, validate the extracted payload
base64Data = base64Data.Trim();
// Check for valid length (must be a multiple of 4) and non-empty
if (base64Data.Length == 0 || base64Data.Length % 4 != 0)
{
return false;
}
// The most reliable test is to simply try to convert it.
// The .NET converter is strict and will throw a FormatException
// for invalid characters or incorrect padding.
try
{
Convert.FromBase64String(base64Data);
return true;
}
catch (FormatException)
{
// The string is not a valid Base64 payload.
return false;
}
}
/// <summary>
/// Handles the creation and persistence of SubscriptionPlanDetails for a particular frequency.
/// </summary>
private async Task<ApiResponse<SubscriptionPlanVM>> CreateSubscriptionPlanDetails(SubscriptionPlanDetailsDto? model, SubscriptionPlan plan, Employee loggedInEmployee, PLAN_FREQUENCY frequency)
{
if (model == null)
{
_logger.LogInfo("No plan detail provided for {Frequency} - skipping.", frequency);
return ApiResponse<SubscriptionPlanVM>.ErrorResponse("Invalid", "No data provided for this frequency", 400);
}
await using var _dbContext = await _dbContextFactory.CreateDbContextAsync();
// Fetch currency master record
var currencyMaster = await _dbContext.CurrencyMaster.AsNoTracking().FirstOrDefaultAsync(c => c.Id == model.CurrencyId);
if (currencyMaster == null)
{
_logger.LogWarning("Currency with Id {CurrencyId} not found for plan {PlanId}/{Frequency}.", model.CurrencyId, plan.Id, frequency);
return ApiResponse<SubscriptionPlanVM>.ErrorResponse("Currency not found", "Specified currency not found", 404);
}
// Map to entity and create related feature details
var planDetails = _mapper.Map<SubscriptionPlanDetails>(model);
var features = _mapper.Map<FeatureDetails>(model.Features);
try
{
await _featureDetailsHelper.AddFeatureDetails(features);
_logger.LogInfo("FeatureDetails for plan {PlanId}/{Frequency} saved in MongoDB.", plan.Id, frequency);
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception occurred while saving features in MongoDB for {PlanId}/{Frequency}.", plan.Id, frequency);
return ApiResponse<SubscriptionPlanVM>.ErrorResponse("Internal error occurred", ExceptionMapper(ex), 500);
}
planDetails.PlanId = plan.Id;
planDetails.Frequency = frequency;
planDetails.FeaturesId = features.Id;
planDetails.CreatedById = loggedInEmployee.Id;
planDetails.CreateAt = DateTime.UtcNow;
_dbContext.SubscriptionPlanDetails.Add(planDetails);
// Prepare view model
var VM = _mapper.Map<SubscriptionPlanVM>(planDetails);
VM.PlanName = plan.PlanName;
VM.Description = plan.Description;
VM.Features = features;
VM.Currency = currencyMaster;
try
{
await _dbContext.SaveChangesAsync();
_logger.LogInfo("Subscription plan details for {PlanId}/{Frequency} saved to SQL.", plan.Id, frequency);
}
catch (DbUpdateException dbEx)
{
_logger.LogError(dbEx, "Database exception occurred while saving plan details for {PlanId}/{Frequency}.", plan.Id, frequency);
return ApiResponse<SubscriptionPlanVM>.ErrorResponse("Internal error occurred", ExceptionMapper(dbEx), 500);
}
return ApiResponse<SubscriptionPlanVM>.SuccessResponse(VM, "Success", 200);
}
#endregion
}
}

View File

@ -50,5 +50,13 @@
"SerilogDatabaseUrl": "mongodb://localhost:27017/DotNetLogs",
"ConnectionString": "mongodb://devuser:DevPass123@147.93.98.152:27017/MarcoBMSCacheLocalDev?authSource=admin&replicaSet=rs01",
"ModificationConnectionString": "mongodb://devuser:DevPass123@147.93.98.152:27017/MarcoBMSLocalDev?authSource=admin&replicaSet=rs01&directConnection=true"
},
"Razorpay": {
"Key": "rzp_test_RXCzgEcXucbuAi",
"Secret": "YNAVBXxRsDg8Oat4M1C3m09W"
},
"Encryption": {
"PaymentKey": "+V47lEWiolUZOUZcHq/3M6SEd3kPraGJpnJ+K5ni0Oo=",
"CollectionKey": "9bVvYrbL1uB+v6TjWRsJ8N8VFI8rE7e7hVhVSKg3JZU="
}
}