feat: integrate PYMS payment gateway, add blog/sitepage/image services, local file manager
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 8m44s
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 8m44s
Payment Gateway: - Add PYMSPaymentService: IPaymentGatewayService via gRPC to PYMS microservice - Add ZarinPalPaymentService: direct ZarinPal integration (backup) - Register 'pyms' payment provider in DI ConfigureServices - Add PYMS proto files (pyms_transaction.proto, pyms_public_messages.proto) - Fix VerifyDiscountWalletCharge: pass 'OK' as status instead of Authority - Update appsettings: PaymentProvider=pyms, sandbox mode, merchant ID Blog System: - Add BlogCategory, BlogPost, BlogPostImage entities and CQRS - Add proto files and gRPC services for blog management - Add Mapster profiles for blog responses Content Management: - Add SitePage entity and CQRS for static pages - Add proto and gRPC service for site pages Image/File Management: - Add LocalFileManager with disk storage + base64 serving + FMS fallback - Add ImagePathResolverInterceptor for gRPC responses - Add ImageResolverService for explicit image resolution - Add UploadsController for public file serving with FMS fallback - Add PaymentCallbackController for discount order payment callbacks Database: - Add blog and content entity migrations - Remove ImagePath MaxLength constraints - Remove old FileManagementService (replaced by LocalFileManager)
This commit is contained in:
@@ -37,7 +37,8 @@ public static class ConfigureServices
|
||||
services.AddScoped<IAlertService, AlertService>();
|
||||
services.AddScoped<IUserNotificationService, UserNotificationService>();
|
||||
services.AddScoped<IKavenegarService, KavenegarService>();
|
||||
services.AddScoped<IFileManagementService, FileManagementService>();
|
||||
// Local file manager — files are saved to wwwroot/uploads/ on CMS disk
|
||||
services.AddSingleton<CMSMicroservice.Application.Common.FileManager.IFileManager, LocalFileManager>();
|
||||
services.AddScoped<IPermissionService, PermissionService>();
|
||||
|
||||
// Daya Loan API Service - قابل تغییر بین Mock و Real
|
||||
@@ -73,19 +74,31 @@ public static class ConfigureServices
|
||||
});
|
||||
}
|
||||
|
||||
// Payment Gateway Service - فقط Daya (درگاه اینترنتی از Gateway میاد نه CMS)
|
||||
var useRealPaymentGateway = configuration.GetValue<bool>("UseRealPaymentGateway", false);
|
||||
// Payment Gateway Service - Multi-Provider Architecture
|
||||
// پشتیبانی از درگاههای مختلف: ZarinPal, Daya, PYMS, Mock
|
||||
var paymentProvider = configuration.GetValue<string>("PaymentProvider", "Mock")?.ToLowerInvariant();
|
||||
|
||||
if (useRealPaymentGateway)
|
||||
switch (paymentProvider)
|
||||
{
|
||||
// فقط Daya برای پرداخت به کاربران (Payout)
|
||||
services.AddHttpClient<IPaymentGatewayService, DayaPaymentService>()
|
||||
.SetHandlerLifetime(TimeSpan.FromMinutes(5));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Mock برای Development و Testing
|
||||
services.AddScoped<IPaymentGatewayService, MockPaymentGatewayService>();
|
||||
case "zarinpal":
|
||||
services.AddHttpClient<IPaymentGatewayService, ZarinPalPaymentService>()
|
||||
.SetHandlerLifetime(TimeSpan.FromMinutes(5));
|
||||
break;
|
||||
|
||||
case "daya":
|
||||
services.AddHttpClient<IPaymentGatewayService, DayaPaymentService>()
|
||||
.SetHandlerLifetime(TimeSpan.FromMinutes(5));
|
||||
break;
|
||||
|
||||
case "pyms":
|
||||
// PYMS (Payment Microservice) — ارتباط gRPC با سرویس پرداخت مستقل
|
||||
services.AddSingleton<IPaymentGatewayService, PYMSPaymentService>();
|
||||
break;
|
||||
|
||||
case "mock":
|
||||
default:
|
||||
services.AddScoped<IPaymentGatewayService, MockPaymentGatewayService>();
|
||||
break;
|
||||
}
|
||||
|
||||
services.AddScoped<IApplicationDbContext>(p => p.GetRequiredService<ApplicationDbContext>());
|
||||
|
||||
@@ -2,6 +2,8 @@ using System.Reflection;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using Microsoft.EntityFrameworkCore.Diagnostics;
|
||||
using CMSMicroservice.Domain.Entities;
|
||||
using CMSMicroservice.Domain.Entities.Blog;
|
||||
using CMSMicroservice.Domain.Entities.Content;
|
||||
using CMSMicroservice.Domain.Entities.Payment;
|
||||
using CMSMicroservice.Domain.Entities.Geography;
|
||||
using CMSMicroservice.Domain.Entities.Order;
|
||||
@@ -138,4 +140,15 @@ public class ApplicationDbContext : DbContext, IApplicationDbContext
|
||||
public DbSet<Warehouse> Warehouses => Set<Warehouse>();
|
||||
public DbSet<InventoryItem> InventoryItems => Set<InventoryItem>();
|
||||
public DbSet<StockMovement> StockMovements => Set<StockMovement>();
|
||||
|
||||
// ============= Blog DbSets =============
|
||||
public DbSet<BlogPost> BlogPosts => Set<BlogPost>();
|
||||
public DbSet<BlogCategory> BlogCategories => Set<BlogCategory>();
|
||||
public DbSet<BlogPostCategory> BlogPostCategories => Set<BlogPostCategory>();
|
||||
public DbSet<BlogPostTag> BlogPostTags => Set<BlogPostTag>();
|
||||
public DbSet<BlogPostImage> BlogPostImages => Set<BlogPostImage>();
|
||||
|
||||
// ============= Content Management DbSets =============
|
||||
public DbSet<SitePage> SitePages => Set<SitePage>();
|
||||
public DbSet<SitePageSection> SitePageSections => Set<SitePageSection>();
|
||||
}
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
using CMSMicroservice.Domain.Entities.Blog;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace CMSMicroservice.Infrastructure.Persistence.Configurations.Blog;
|
||||
|
||||
public class BlogCategoryConfiguration : IEntityTypeConfiguration<BlogCategory>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<BlogCategory> builder)
|
||||
{
|
||||
builder.ToTable("BlogCategories");
|
||||
builder.HasKey(x => x.Id);
|
||||
|
||||
builder.Property(x => x.Title).IsRequired().HasMaxLength(100);
|
||||
builder.Property(x => x.Slug).IsRequired().HasMaxLength(100);
|
||||
builder.Property(x => x.Description).HasMaxLength(500);
|
||||
builder.Property(x => x.IconName).HasMaxLength(100);
|
||||
builder.Property(x => x.SortOrder).IsRequired().HasDefaultValue(0);
|
||||
builder.Property(x => x.IsActive).IsRequired().HasDefaultValue(true);
|
||||
|
||||
// Indexes
|
||||
builder.HasIndex(x => x.Slug).IsUnique().HasDatabaseName("IX_BlogCategories_Slug");
|
||||
builder.HasIndex(x => x.IsActive).HasDatabaseName("IX_BlogCategories_IsActive");
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
using CMSMicroservice.Domain.Entities.Blog;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace CMSMicroservice.Infrastructure.Persistence.Configurations.Blog;
|
||||
|
||||
public class BlogPostCategoryConfiguration : IEntityTypeConfiguration<BlogPostCategory>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<BlogPostCategory> builder)
|
||||
{
|
||||
builder.ToTable("BlogPostCategories");
|
||||
builder.HasKey(e => e.Id);
|
||||
|
||||
builder.HasOne(d => d.BlogPost)
|
||||
.WithMany(p => p.BlogPostCategories)
|
||||
.HasForeignKey(d => d.BlogPostId);
|
||||
|
||||
builder.HasOne(d => d.BlogCategory)
|
||||
.WithMany(p => p.BlogPostCategories)
|
||||
.HasForeignKey(d => d.BlogCategoryId);
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
using CMSMicroservice.Domain.Entities.Blog;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace CMSMicroservice.Infrastructure.Persistence.Configurations.Blog;
|
||||
|
||||
public class BlogPostConfiguration : IEntityTypeConfiguration<BlogPost>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<BlogPost> builder)
|
||||
{
|
||||
builder.ToTable("BlogPosts");
|
||||
builder.HasKey(x => x.Id);
|
||||
|
||||
builder.Property(x => x.Title).IsRequired().HasMaxLength(200);
|
||||
builder.Property(x => x.Slug).IsRequired().HasMaxLength(200);
|
||||
builder.Property(x => x.Summary).HasMaxLength(500);
|
||||
builder.Property(x => x.HtmlContent).IsRequired();
|
||||
builder.Property(x => x.FeaturedImagePath);
|
||||
builder.Property(x => x.FeaturedImageThumbnailPath);
|
||||
builder.Property(x => x.Status).IsRequired();
|
||||
builder.Property(x => x.ViewCount).IsRequired().HasDefaultValue(0);
|
||||
builder.Property(x => x.AuthorUserId).IsRequired();
|
||||
builder.Property(x => x.IsFeatured).IsRequired().HasDefaultValue(false);
|
||||
builder.Property(x => x.SortOrder).IsRequired().HasDefaultValue(0);
|
||||
|
||||
// Indexes
|
||||
builder.HasIndex(x => x.Slug).IsUnique().HasDatabaseName("IX_BlogPosts_Slug");
|
||||
builder.HasIndex(x => x.Status).HasDatabaseName("IX_BlogPosts_Status");
|
||||
builder.HasIndex(x => x.PublishedAt).HasDatabaseName("IX_BlogPosts_PublishedAt");
|
||||
builder.HasIndex(x => x.IsFeatured).HasDatabaseName("IX_BlogPosts_IsFeatured");
|
||||
builder.HasIndex(x => x.AuthorUserId).HasDatabaseName("IX_BlogPosts_AuthorUserId");
|
||||
builder.HasIndex(x => new { x.Status, x.PublishedAt })
|
||||
.HasDatabaseName("IX_BlogPosts_Status_PublishedAt");
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
using CMSMicroservice.Domain.Entities.Blog;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace CMSMicroservice.Infrastructure.Persistence.Configurations.Blog;
|
||||
|
||||
public class BlogPostImageConfiguration : IEntityTypeConfiguration<BlogPostImage>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<BlogPostImage> builder)
|
||||
{
|
||||
builder.ToTable("BlogPostImages");
|
||||
builder.HasKey(x => x.Id);
|
||||
|
||||
builder.Property(x => x.BlogPostId).IsRequired();
|
||||
builder.Property(x => x.ImagePath).IsRequired();
|
||||
builder.Property(x => x.ThumbnailPath).IsRequired();
|
||||
builder.Property(x => x.AltText).HasMaxLength(200);
|
||||
builder.Property(x => x.Caption).HasMaxLength(300);
|
||||
builder.Property(x => x.SortOrder).IsRequired().HasDefaultValue(0);
|
||||
|
||||
builder.HasOne(d => d.BlogPost)
|
||||
.WithMany(p => p.BlogPostImages)
|
||||
.HasForeignKey(d => d.BlogPostId);
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
using CMSMicroservice.Domain.Entities.Blog;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace CMSMicroservice.Infrastructure.Persistence.Configurations.Blog;
|
||||
|
||||
public class BlogPostTagConfiguration : IEntityTypeConfiguration<BlogPostTag>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<BlogPostTag> builder)
|
||||
{
|
||||
builder.ToTable("BlogPostTags");
|
||||
builder.HasKey(e => e.Id);
|
||||
|
||||
builder.HasOne(d => d.BlogPost)
|
||||
.WithMany(p => p.BlogPostTags)
|
||||
.HasForeignKey(d => d.BlogPostId);
|
||||
|
||||
builder.HasOne(d => d.Tag)
|
||||
.WithMany()
|
||||
.HasForeignKey(d => d.TagId);
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
using CMSMicroservice.Domain.Entities.Content;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace CMSMicroservice.Infrastructure.Persistence.Configurations.Content;
|
||||
|
||||
public class SitePageConfiguration : IEntityTypeConfiguration<SitePage>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<SitePage> builder)
|
||||
{
|
||||
builder.ToTable("SitePages");
|
||||
builder.HasKey(x => x.Id);
|
||||
|
||||
builder.Property(x => x.PageKey).IsRequired().HasMaxLength(50);
|
||||
builder.Property(x => x.Title).IsRequired().HasMaxLength(200);
|
||||
builder.Property(x => x.MetaDescription).HasMaxLength(300);
|
||||
builder.Property(x => x.HeroTitle).HasMaxLength(200);
|
||||
builder.Property(x => x.HeroSubtitle).HasMaxLength(500);
|
||||
builder.Property(x => x.HeroImagePath);
|
||||
builder.Property(x => x.IsActive).IsRequired().HasDefaultValue(true);
|
||||
|
||||
// Indexes
|
||||
builder.HasIndex(x => x.PageKey).IsUnique().HasDatabaseName("IX_SitePages_PageKey");
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
using CMSMicroservice.Domain.Entities.Content;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace CMSMicroservice.Infrastructure.Persistence.Configurations.Content;
|
||||
|
||||
public class SitePageSectionConfiguration : IEntityTypeConfiguration<SitePageSection>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<SitePageSection> builder)
|
||||
{
|
||||
builder.ToTable("SitePageSections");
|
||||
builder.HasKey(x => x.Id);
|
||||
|
||||
builder.Property(x => x.SitePageId).IsRequired();
|
||||
builder.Property(x => x.SectionKey).IsRequired().HasMaxLength(100);
|
||||
builder.Property(x => x.Title).IsRequired().HasMaxLength(200);
|
||||
builder.Property(x => x.Subtitle).HasMaxLength(300);
|
||||
builder.Property(x => x.IconName).HasMaxLength(100);
|
||||
builder.Property(x => x.ImagePath);
|
||||
builder.Property(x => x.ImageThumbnailPath);
|
||||
builder.Property(x => x.SortOrder).IsRequired().HasDefaultValue(0);
|
||||
builder.Property(x => x.IsActive).IsRequired().HasDefaultValue(true);
|
||||
|
||||
builder.HasOne(d => d.SitePage)
|
||||
.WithMany(p => p.Sections)
|
||||
.HasForeignKey(d => d.SitePageId);
|
||||
|
||||
// Indexes
|
||||
builder.HasIndex(x => new { x.SitePageId, x.SectionKey })
|
||||
.HasDatabaseName("IX_SitePageSections_PageId_SectionKey");
|
||||
}
|
||||
}
|
||||
+1
-2
@@ -28,8 +28,7 @@ public class DiscountCategoryConfiguration : IEntityTypeConfiguration<DiscountCa
|
||||
builder.Property(entity => entity.Description)
|
||||
.HasMaxLength(1000);
|
||||
|
||||
builder.Property(entity => entity.ImagePath)
|
||||
.HasMaxLength(500);
|
||||
builder.Property(entity => entity.ImagePath);
|
||||
|
||||
builder.Property(entity => entity.IsActive)
|
||||
.IsRequired()
|
||||
|
||||
+2
-4
@@ -36,12 +36,10 @@ public class DiscountProductConfiguration : IEntityTypeConfiguration<DiscountPro
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(entity => entity.ImagePath)
|
||||
.IsRequired()
|
||||
.HasMaxLength(500);
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(entity => entity.ThumbnailPath)
|
||||
.IsRequired()
|
||||
.HasMaxLength(500);
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(entity => entity.IsActive)
|
||||
.IsRequired()
|
||||
|
||||
+2
-4
@@ -19,11 +19,9 @@ public class DiscountProductImageConfiguration : IEntityTypeConfiguration<Discou
|
||||
.HasMaxLength(500);
|
||||
|
||||
builder.Property(x => x.ImagePath)
|
||||
.IsRequired()
|
||||
.HasMaxLength(500);
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(x => x.ThumbnailPath)
|
||||
.HasMaxLength(500);
|
||||
builder.Property(x => x.ThumbnailPath);
|
||||
|
||||
builder.HasOne(x => x.DiscountProduct)
|
||||
.WithMany(p => p.Images)
|
||||
|
||||
+1
@@ -8,6 +8,7 @@ public class ManualPaymentConfiguration : IEntityTypeConfiguration<ManualPayment
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<ManualPayment> builder)
|
||||
{
|
||||
builder.HasQueryFilter(p => !p.IsDeleted);
|
||||
builder.ToTable("ManualPayments");
|
||||
|
||||
builder.HasKey(x => x.Id);
|
||||
|
||||
+2
-1
@@ -8,6 +8,7 @@ public class OrderVATConfiguration : IEntityTypeConfiguration<OrderVAT>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<OrderVAT> builder)
|
||||
{
|
||||
builder.HasQueryFilter(p => !p.IsDeleted);
|
||||
builder.ToTable("OrderVATs");
|
||||
|
||||
builder.HasKey(x => x.Id);
|
||||
@@ -37,7 +38,7 @@ public class OrderVATConfiguration : IEntityTypeConfiguration<OrderVAT>
|
||||
|
||||
// Foreign Key
|
||||
builder.HasOne(x => x.Order)
|
||||
.WithOne()
|
||||
.WithOne(x => x.OrderVAT)
|
||||
.HasForeignKey<OrderVAT>(x => x.OrderId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
|
||||
+1
@@ -7,6 +7,7 @@ public class ProductCategoryConfiguration : IEntityTypeConfiguration<ProductCate
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<ProductCategory> builder)
|
||||
{
|
||||
builder.HasQueryFilter(p => !p.IsDeleted);
|
||||
builder.ToTable("ProductCategories", "CMS");
|
||||
builder.HasKey(e => e.Id);
|
||||
|
||||
|
||||
+1
@@ -7,6 +7,7 @@ public class ProductTagConfiguration : IEntityTypeConfiguration<ProductTag>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<ProductTag> builder)
|
||||
{
|
||||
builder.HasQueryFilter(p => !p.IsDeleted);
|
||||
builder.ToTable("ProductTags", "CMS");
|
||||
builder.HasKey(e => e.Id);
|
||||
|
||||
|
||||
+1
@@ -8,6 +8,7 @@ public class PublicMessageConfiguration : IEntityTypeConfiguration<PublicMessage
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<PublicMessage> builder)
|
||||
{
|
||||
builder.HasQueryFilter(p => !p.IsDeleted);
|
||||
builder.ToTable("PublicMessages");
|
||||
|
||||
builder.HasKey(x => x.Id);
|
||||
|
||||
+4431
File diff suppressed because it is too large
Load Diff
+345
@@ -0,0 +1,345 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace CMSMicroservice.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddBlogAndContentEntities : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "BlogCategories",
|
||||
schema: "CMS",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
Title = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
|
||||
Slug = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
|
||||
Description = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
|
||||
IconName = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
|
||||
SortOrder = table.Column<int>(type: "int", nullable: false, defaultValue: 0),
|
||||
IsActive = table.Column<bool>(type: "bit", nullable: false, defaultValue: true),
|
||||
Created = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
CreatedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
LastModified = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
LastModifiedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
IsDeleted = table.Column<bool>(type: "bit", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_BlogCategories", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "BlogPosts",
|
||||
schema: "CMS",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
Title = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
|
||||
Slug = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
|
||||
Summary = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
|
||||
HtmlContent = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||
FeaturedImagePath = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
|
||||
FeaturedImageThumbnailPath = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
|
||||
Status = table.Column<int>(type: "int", nullable: false),
|
||||
PublishedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
ScheduledPublishAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
ViewCount = table.Column<int>(type: "int", nullable: false, defaultValue: 0),
|
||||
AuthorUserId = table.Column<long>(type: "bigint", nullable: false),
|
||||
IsFeatured = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
|
||||
SortOrder = table.Column<int>(type: "int", nullable: false, defaultValue: 0),
|
||||
Created = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
CreatedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
LastModified = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
LastModifiedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
IsDeleted = table.Column<bool>(type: "bit", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_BlogPosts", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "SitePages",
|
||||
schema: "CMS",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
PageKey = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
|
||||
Title = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
|
||||
MetaDescription = table.Column<string>(type: "nvarchar(300)", maxLength: 300, nullable: true),
|
||||
HeroTitle = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: true),
|
||||
HeroSubtitle = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
|
||||
HeroImagePath = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
|
||||
IsActive = table.Column<bool>(type: "bit", nullable: false, defaultValue: true),
|
||||
Created = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
CreatedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
LastModified = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
LastModifiedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
IsDeleted = table.Column<bool>(type: "bit", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_SitePages", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "BlogPostCategories",
|
||||
schema: "CMS",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
BlogPostId = table.Column<long>(type: "bigint", nullable: false),
|
||||
BlogCategoryId = table.Column<long>(type: "bigint", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_BlogPostCategories", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_BlogPostCategories_BlogCategories_BlogCategoryId",
|
||||
column: x => x.BlogCategoryId,
|
||||
principalSchema: "CMS",
|
||||
principalTable: "BlogCategories",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_BlogPostCategories_BlogPosts_BlogPostId",
|
||||
column: x => x.BlogPostId,
|
||||
principalSchema: "CMS",
|
||||
principalTable: "BlogPosts",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "BlogPostImages",
|
||||
schema: "CMS",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
BlogPostId = table.Column<long>(type: "bigint", nullable: false),
|
||||
ImagePath = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: false),
|
||||
ThumbnailPath = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: false),
|
||||
AltText = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: true),
|
||||
Caption = table.Column<string>(type: "nvarchar(300)", maxLength: 300, nullable: true),
|
||||
SortOrder = table.Column<int>(type: "int", nullable: false, defaultValue: 0),
|
||||
Created = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
CreatedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
LastModified = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
LastModifiedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
IsDeleted = table.Column<bool>(type: "bit", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_BlogPostImages", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_BlogPostImages_BlogPosts_BlogPostId",
|
||||
column: x => x.BlogPostId,
|
||||
principalSchema: "CMS",
|
||||
principalTable: "BlogPosts",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "BlogPostTags",
|
||||
schema: "CMS",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
BlogPostId = table.Column<long>(type: "bigint", nullable: false),
|
||||
TagId = table.Column<long>(type: "bigint", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_BlogPostTags", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_BlogPostTags_BlogPosts_BlogPostId",
|
||||
column: x => x.BlogPostId,
|
||||
principalSchema: "CMS",
|
||||
principalTable: "BlogPosts",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_BlogPostTags_Tags_TagId",
|
||||
column: x => x.TagId,
|
||||
principalSchema: "CMS",
|
||||
principalTable: "Tags",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "SitePageSections",
|
||||
schema: "CMS",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
SitePageId = table.Column<long>(type: "bigint", nullable: false),
|
||||
SectionKey = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
|
||||
Title = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
|
||||
Subtitle = table.Column<string>(type: "nvarchar(300)", maxLength: 300, nullable: true),
|
||||
HtmlContent = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
IconName = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
|
||||
ImagePath = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
|
||||
ImageThumbnailPath = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
|
||||
SortOrder = table.Column<int>(type: "int", nullable: false, defaultValue: 0),
|
||||
IsActive = table.Column<bool>(type: "bit", nullable: false, defaultValue: true),
|
||||
ExtraData = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
Created = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
CreatedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
LastModified = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
LastModifiedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
IsDeleted = table.Column<bool>(type: "bit", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_SitePageSections", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_SitePageSections_SitePages_SitePageId",
|
||||
column: x => x.SitePageId,
|
||||
principalSchema: "CMS",
|
||||
principalTable: "SitePages",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BlogCategories_IsActive",
|
||||
schema: "CMS",
|
||||
table: "BlogCategories",
|
||||
column: "IsActive");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BlogCategories_Slug",
|
||||
schema: "CMS",
|
||||
table: "BlogCategories",
|
||||
column: "Slug",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BlogPostCategories_BlogCategoryId",
|
||||
schema: "CMS",
|
||||
table: "BlogPostCategories",
|
||||
column: "BlogCategoryId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BlogPostCategories_BlogPostId",
|
||||
schema: "CMS",
|
||||
table: "BlogPostCategories",
|
||||
column: "BlogPostId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BlogPostImages_BlogPostId",
|
||||
schema: "CMS",
|
||||
table: "BlogPostImages",
|
||||
column: "BlogPostId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BlogPosts_AuthorUserId",
|
||||
schema: "CMS",
|
||||
table: "BlogPosts",
|
||||
column: "AuthorUserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BlogPosts_IsFeatured",
|
||||
schema: "CMS",
|
||||
table: "BlogPosts",
|
||||
column: "IsFeatured");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BlogPosts_PublishedAt",
|
||||
schema: "CMS",
|
||||
table: "BlogPosts",
|
||||
column: "PublishedAt");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BlogPosts_Slug",
|
||||
schema: "CMS",
|
||||
table: "BlogPosts",
|
||||
column: "Slug",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BlogPosts_Status",
|
||||
schema: "CMS",
|
||||
table: "BlogPosts",
|
||||
column: "Status");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BlogPosts_Status_PublishedAt",
|
||||
schema: "CMS",
|
||||
table: "BlogPosts",
|
||||
columns: new[] { "Status", "PublishedAt" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BlogPostTags_BlogPostId",
|
||||
schema: "CMS",
|
||||
table: "BlogPostTags",
|
||||
column: "BlogPostId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BlogPostTags_TagId",
|
||||
schema: "CMS",
|
||||
table: "BlogPostTags",
|
||||
column: "TagId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_SitePages_PageKey",
|
||||
schema: "CMS",
|
||||
table: "SitePages",
|
||||
column: "PageKey",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_SitePageSections_PageId_SectionKey",
|
||||
schema: "CMS",
|
||||
table: "SitePageSections",
|
||||
columns: new[] { "SitePageId", "SectionKey" });
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "BlogPostCategories",
|
||||
schema: "CMS");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "BlogPostImages",
|
||||
schema: "CMS");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "BlogPostTags",
|
||||
schema: "CMS");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "SitePageSections",
|
||||
schema: "CMS");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "BlogCategories",
|
||||
schema: "CMS");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "BlogPosts",
|
||||
schema: "CMS");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "SitePages",
|
||||
schema: "CMS");
|
||||
}
|
||||
}
|
||||
}
|
||||
+4410
File diff suppressed because it is too large
Load Diff
+309
@@ -0,0 +1,309 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace CMSMicroservice.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class RemoveImagePathMaxLength : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_UserOrders_OrderVATs_OrderVATId",
|
||||
schema: "CMS",
|
||||
table: "UserOrders");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_UserOrders_OrderVATId",
|
||||
schema: "CMS",
|
||||
table: "UserOrders");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "OrderVATId",
|
||||
schema: "CMS",
|
||||
table: "UserOrders");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "ImageThumbnailPath",
|
||||
schema: "CMS",
|
||||
table: "SitePageSections",
|
||||
type: "nvarchar(max)",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "nvarchar(500)",
|
||||
oldMaxLength: 500,
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "ImagePath",
|
||||
schema: "CMS",
|
||||
table: "SitePageSections",
|
||||
type: "nvarchar(max)",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "nvarchar(500)",
|
||||
oldMaxLength: 500,
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "HeroImagePath",
|
||||
schema: "CMS",
|
||||
table: "SitePages",
|
||||
type: "nvarchar(max)",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "nvarchar(500)",
|
||||
oldMaxLength: 500,
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "ThumbnailPath",
|
||||
schema: "CMS",
|
||||
table: "DiscountProducts",
|
||||
type: "nvarchar(max)",
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "nvarchar(500)",
|
||||
oldMaxLength: 500);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "ImagePath",
|
||||
schema: "CMS",
|
||||
table: "DiscountProducts",
|
||||
type: "nvarchar(max)",
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "nvarchar(500)",
|
||||
oldMaxLength: 500);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "ThumbnailPath",
|
||||
schema: "CMS",
|
||||
table: "DiscountProductImages",
|
||||
type: "nvarchar(max)",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "nvarchar(500)",
|
||||
oldMaxLength: 500,
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "ImagePath",
|
||||
schema: "CMS",
|
||||
table: "DiscountProductImages",
|
||||
type: "nvarchar(max)",
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "nvarchar(500)",
|
||||
oldMaxLength: 500);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "ImagePath",
|
||||
schema: "CMS",
|
||||
table: "DiscountCategories",
|
||||
type: "nvarchar(max)",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "nvarchar(500)",
|
||||
oldMaxLength: 500,
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "FeaturedImageThumbnailPath",
|
||||
schema: "CMS",
|
||||
table: "BlogPosts",
|
||||
type: "nvarchar(max)",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "nvarchar(500)",
|
||||
oldMaxLength: 500,
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "FeaturedImagePath",
|
||||
schema: "CMS",
|
||||
table: "BlogPosts",
|
||||
type: "nvarchar(max)",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "nvarchar(500)",
|
||||
oldMaxLength: 500,
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "ThumbnailPath",
|
||||
schema: "CMS",
|
||||
table: "BlogPostImages",
|
||||
type: "nvarchar(max)",
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "nvarchar(500)",
|
||||
oldMaxLength: 500);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "ImagePath",
|
||||
schema: "CMS",
|
||||
table: "BlogPostImages",
|
||||
type: "nvarchar(max)",
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "nvarchar(500)",
|
||||
oldMaxLength: 500);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<long>(
|
||||
name: "OrderVATId",
|
||||
schema: "CMS",
|
||||
table: "UserOrders",
|
||||
type: "bigint",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "ImageThumbnailPath",
|
||||
schema: "CMS",
|
||||
table: "SitePageSections",
|
||||
type: "nvarchar(500)",
|
||||
maxLength: 500,
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "nvarchar(max)",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "ImagePath",
|
||||
schema: "CMS",
|
||||
table: "SitePageSections",
|
||||
type: "nvarchar(500)",
|
||||
maxLength: 500,
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "nvarchar(max)",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "HeroImagePath",
|
||||
schema: "CMS",
|
||||
table: "SitePages",
|
||||
type: "nvarchar(500)",
|
||||
maxLength: 500,
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "nvarchar(max)",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "ThumbnailPath",
|
||||
schema: "CMS",
|
||||
table: "DiscountProducts",
|
||||
type: "nvarchar(500)",
|
||||
maxLength: 500,
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "nvarchar(max)");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "ImagePath",
|
||||
schema: "CMS",
|
||||
table: "DiscountProducts",
|
||||
type: "nvarchar(500)",
|
||||
maxLength: 500,
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "nvarchar(max)");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "ThumbnailPath",
|
||||
schema: "CMS",
|
||||
table: "DiscountProductImages",
|
||||
type: "nvarchar(500)",
|
||||
maxLength: 500,
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "nvarchar(max)",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "ImagePath",
|
||||
schema: "CMS",
|
||||
table: "DiscountProductImages",
|
||||
type: "nvarchar(500)",
|
||||
maxLength: 500,
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "nvarchar(max)");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "ImagePath",
|
||||
schema: "CMS",
|
||||
table: "DiscountCategories",
|
||||
type: "nvarchar(500)",
|
||||
maxLength: 500,
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "nvarchar(max)",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "FeaturedImageThumbnailPath",
|
||||
schema: "CMS",
|
||||
table: "BlogPosts",
|
||||
type: "nvarchar(500)",
|
||||
maxLength: 500,
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "nvarchar(max)",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "FeaturedImagePath",
|
||||
schema: "CMS",
|
||||
table: "BlogPosts",
|
||||
type: "nvarchar(500)",
|
||||
maxLength: 500,
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "nvarchar(max)",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "ThumbnailPath",
|
||||
schema: "CMS",
|
||||
table: "BlogPostImages",
|
||||
type: "nvarchar(500)",
|
||||
maxLength: 500,
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "nvarchar(max)");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "ImagePath",
|
||||
schema: "CMS",
|
||||
table: "BlogPostImages",
|
||||
type: "nvarchar(500)",
|
||||
maxLength: 500,
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "nvarchar(max)");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_UserOrders_OrderVATId",
|
||||
schema: "CMS",
|
||||
table: "UserOrders",
|
||||
column: "OrderVATId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_UserOrders_OrderVATs_OrderVATId",
|
||||
schema: "CMS",
|
||||
table: "UserOrders",
|
||||
column: "OrderVATId",
|
||||
principalSchema: "CMS",
|
||||
principalTable: "OrderVATs",
|
||||
principalColumn: "Id");
|
||||
}
|
||||
}
|
||||
}
|
||||
+484
-22
@@ -23,6 +23,267 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Blog.BlogCategory", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<string>("IconName")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(true);
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTime?>("LastModified")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("LastModifiedBy")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int")
|
||||
.HasDefaultValue(0);
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("IsActive")
|
||||
.HasDatabaseName("IX_BlogCategories_IsActive");
|
||||
|
||||
b.HasIndex("Slug")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("IX_BlogCategories_Slug");
|
||||
|
||||
b.ToTable("BlogCategories", "CMS");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Blog.BlogPost", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<long>("AuthorUserId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("FeaturedImagePath")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("FeaturedImageThumbnailPath")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("HtmlContent")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool>("IsFeatured")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<DateTime?>("LastModified")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("LastModifiedBy")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTime?>("PublishedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime?>("ScheduledPublishAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int")
|
||||
.HasDefaultValue(0);
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Summary")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<int>("ViewCount")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int")
|
||||
.HasDefaultValue(0);
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AuthorUserId")
|
||||
.HasDatabaseName("IX_BlogPosts_AuthorUserId");
|
||||
|
||||
b.HasIndex("IsFeatured")
|
||||
.HasDatabaseName("IX_BlogPosts_IsFeatured");
|
||||
|
||||
b.HasIndex("PublishedAt")
|
||||
.HasDatabaseName("IX_BlogPosts_PublishedAt");
|
||||
|
||||
b.HasIndex("Slug")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("IX_BlogPosts_Slug");
|
||||
|
||||
b.HasIndex("Status")
|
||||
.HasDatabaseName("IX_BlogPosts_Status");
|
||||
|
||||
b.HasIndex("Status", "PublishedAt")
|
||||
.HasDatabaseName("IX_BlogPosts_Status_PublishedAt");
|
||||
|
||||
b.ToTable("BlogPosts", "CMS");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Blog.BlogPostCategory", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<long>("BlogCategoryId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long>("BlogPostId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BlogCategoryId");
|
||||
|
||||
b.HasIndex("BlogPostId");
|
||||
|
||||
b.ToTable("BlogPostCategories", "CMS");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Blog.BlogPostImage", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<string>("AltText")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<long>("BlogPostId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("Caption")
|
||||
.HasMaxLength(300)
|
||||
.HasColumnType("nvarchar(300)");
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ImagePath")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTime?>("LastModified")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("LastModifiedBy")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int")
|
||||
.HasDefaultValue(0);
|
||||
|
||||
b.Property<string>("ThumbnailPath")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BlogPostId");
|
||||
|
||||
b.ToTable("BlogPostImages", "CMS");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Blog.BlogPostTag", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<long>("BlogPostId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long>("TagId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BlogPostId");
|
||||
|
||||
b.HasIndex("TagId");
|
||||
|
||||
b.ToTable("BlogPostTags", "CMS");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Category", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
@@ -503,6 +764,142 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
|
||||
b.ToTable("AppVersions", "CMS");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Content.SitePage", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("HeroImagePath")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("HeroSubtitle")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<string>("HeroTitle")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(true);
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTime?>("LastModified")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("LastModifiedBy")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("MetaDescription")
|
||||
.HasMaxLength(300)
|
||||
.HasColumnType("nvarchar(300)");
|
||||
|
||||
b.Property<string>("PageKey")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("PageKey")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("IX_SitePages_PageKey");
|
||||
|
||||
b.ToTable("SitePages", "CMS");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Content.SitePageSection", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ExtraData")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("HtmlContent")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("IconName")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("ImagePath")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ImageThumbnailPath")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(true);
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTime?>("LastModified")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("LastModifiedBy")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("SectionKey")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<long>("SitePageId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int")
|
||||
.HasDefaultValue(0);
|
||||
|
||||
b.Property<string>("Subtitle")
|
||||
.HasMaxLength(300)
|
||||
.HasColumnType("nvarchar(300)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SitePageId", "SectionKey")
|
||||
.HasDatabaseName("IX_SitePageSections_PageId_SectionKey");
|
||||
|
||||
b.ToTable("SitePageSections", "CMS");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Contract", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
@@ -622,8 +1019,7 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
|
||||
.HasColumnType("nvarchar(1000)");
|
||||
|
||||
b.Property<string>("ImagePath")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -808,8 +1204,7 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
|
||||
|
||||
b.Property<string>("ImagePath")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -847,8 +1242,7 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
|
||||
|
||||
b.Property<string>("ThumbnailPath")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
@@ -925,8 +1319,7 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
|
||||
|
||||
b.Property<string>("ImagePath")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("bit");
|
||||
@@ -944,8 +1337,7 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ThumbnailPath")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasMaxLength(200)
|
||||
@@ -2767,9 +3159,6 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
|
||||
b.Property<string>("LastModifiedBy")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<long?>("OrderVATId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long?>("PackageId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
@@ -2796,8 +3185,6 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("OrderVATId");
|
||||
|
||||
b.HasIndex("PackageId");
|
||||
|
||||
b.HasIndex("TransactionId");
|
||||
@@ -3167,6 +3554,55 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
|
||||
b.ToTable("WeekDefinitions", "CMS");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Blog.BlogPostCategory", b =>
|
||||
{
|
||||
b.HasOne("CMSMicroservice.Domain.Entities.Blog.BlogCategory", "BlogCategory")
|
||||
.WithMany("BlogPostCategories")
|
||||
.HasForeignKey("BlogCategoryId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("CMSMicroservice.Domain.Entities.Blog.BlogPost", "BlogPost")
|
||||
.WithMany("BlogPostCategories")
|
||||
.HasForeignKey("BlogPostId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("BlogCategory");
|
||||
|
||||
b.Navigation("BlogPost");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Blog.BlogPostImage", b =>
|
||||
{
|
||||
b.HasOne("CMSMicroservice.Domain.Entities.Blog.BlogPost", "BlogPost")
|
||||
.WithMany("BlogPostImages")
|
||||
.HasForeignKey("BlogPostId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("BlogPost");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Blog.BlogPostTag", b =>
|
||||
{
|
||||
b.HasOne("CMSMicroservice.Domain.Entities.Blog.BlogPost", "BlogPost")
|
||||
.WithMany("BlogPostTags")
|
||||
.HasForeignKey("BlogPostId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("CMSMicroservice.Domain.Entities.Tag", "Tag")
|
||||
.WithMany()
|
||||
.HasForeignKey("TagId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("BlogPost");
|
||||
|
||||
b.Navigation("Tag");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Category", b =>
|
||||
{
|
||||
b.HasOne("CMSMicroservice.Domain.Entities.Category", "Parent")
|
||||
@@ -3263,6 +3699,17 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
|
||||
b.Navigation("WeekDefinition");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Content.SitePageSection", b =>
|
||||
{
|
||||
b.HasOne("CMSMicroservice.Domain.Entities.Content.SitePage", "SitePage")
|
||||
.WithMany("Sections")
|
||||
.HasForeignKey("SitePageId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("SitePage");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CMSMicroservice.Domain.Entities.DayaLoanContract", b =>
|
||||
{
|
||||
b.HasOne("CMSMicroservice.Domain.Entities.Transaction", "Transaction")
|
||||
@@ -3502,7 +3949,7 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
|
||||
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Order.OrderVAT", b =>
|
||||
{
|
||||
b.HasOne("CMSMicroservice.Domain.Entities.UserOrder", "Order")
|
||||
.WithOne()
|
||||
.WithOne("OrderVAT")
|
||||
.HasForeignKey("CMSMicroservice.Domain.Entities.Order.OrderVAT", "OrderId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
@@ -3657,10 +4104,6 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
|
||||
|
||||
modelBuilder.Entity("CMSMicroservice.Domain.Entities.UserOrder", b =>
|
||||
{
|
||||
b.HasOne("CMSMicroservice.Domain.Entities.Order.OrderVAT", "OrderVAT")
|
||||
.WithMany()
|
||||
.HasForeignKey("OrderVATId");
|
||||
|
||||
b.HasOne("CMSMicroservice.Domain.Entities.Package", "Package")
|
||||
.WithMany("UserOrders")
|
||||
.HasForeignKey("PackageId");
|
||||
@@ -3681,8 +4124,6 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("OrderVAT");
|
||||
|
||||
b.Navigation("Package");
|
||||
|
||||
b.Navigation("Transaction");
|
||||
@@ -3766,6 +4207,20 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
|
||||
b.Navigation("Wallet");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Blog.BlogCategory", b =>
|
||||
{
|
||||
b.Navigation("BlogPostCategories");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Blog.BlogPost", b =>
|
||||
{
|
||||
b.Navigation("BlogPostCategories");
|
||||
|
||||
b.Navigation("BlogPostImages");
|
||||
|
||||
b.Navigation("BlogPostTags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Category", b =>
|
||||
{
|
||||
b.Navigation("Categories");
|
||||
@@ -3795,6 +4250,11 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
|
||||
b.Navigation("UserCommissionPayouts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Content.SitePage", b =>
|
||||
{
|
||||
b.Navigation("Sections");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Contract", b =>
|
||||
{
|
||||
b.Navigation("UserContracts");
|
||||
@@ -3915,6 +4375,8 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
|
||||
modelBuilder.Entity("CMSMicroservice.Domain.Entities.UserOrder", b =>
|
||||
{
|
||||
b.Navigation("FactorDetails");
|
||||
|
||||
b.Navigation("OrderVAT");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CMSMicroservice.Domain.Entities.UserWallet", b =>
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Protobuf.Protos.FMS;
|
||||
using Google.Protobuf;
|
||||
using Grpc.Net.Client;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Formats.Jpeg;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
using System.IO;
|
||||
|
||||
namespace CMSMicroservice.Infrastructure.Services;
|
||||
|
||||
public class FileManagementService : IFileManagementService, IDisposable
|
||||
{
|
||||
private readonly ILogger<FileManagementService> _logger;
|
||||
private readonly FileInfoContract.FileInfoContractClient _client;
|
||||
private readonly GrpcChannel _channel;
|
||||
|
||||
private const int MainImageMaxWidth = 1200;
|
||||
private const int MainImageMaxHeight = 1200;
|
||||
private const int ThumbnailMaxWidth = 300;
|
||||
private const int ThumbnailMaxHeight = 300;
|
||||
private const int JpegQuality = 75;
|
||||
|
||||
public FileManagementService(IConfiguration configuration, ILogger<FileManagementService> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
|
||||
var fmsAddress = configuration["FMS:Address"] ?? "https://dl.afrino.co";
|
||||
|
||||
_channel = GrpcChannel.ForAddress(fmsAddress, new GrpcChannelOptions
|
||||
{
|
||||
MaxReceiveMessageSize = 100 * 1024 * 1024, // 100 MB
|
||||
MaxSendMessageSize = 100 * 1024 * 1024
|
||||
});
|
||||
|
||||
_client = new FileInfoContract.FileInfoContractClient(_channel);
|
||||
}
|
||||
|
||||
public async Task<string?> UploadFileAsync(string directory, byte[] fileBytes, string mime, string? fileName,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var request = new CreateNewFileInfoRequest
|
||||
{
|
||||
Directory = directory,
|
||||
File = ByteString.CopyFrom(fileBytes),
|
||||
Mime = mime,
|
||||
IsBase64 = false
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(fileName))
|
||||
request.FileName = fileName;
|
||||
|
||||
var response = await _client.CreateNewFileInfoAsync(request, cancellationToken: cancellationToken);
|
||||
|
||||
if (response != null && !string.IsNullOrWhiteSpace(response.File))
|
||||
{
|
||||
_logger.LogInformation("File uploaded to FMS successfully. Id: {Id}, Path: {Path}", response.Id, response.File);
|
||||
return response.File;
|
||||
}
|
||||
|
||||
_logger.LogWarning("FMS upload returned null or empty path for file: {FileName}", fileName);
|
||||
return null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error uploading file to FMS. Directory: {Directory}, FileName: {FileName}", directory, fileName);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<(string? MainImagePath, string? ThumbnailPath)> UploadImageWithThumbnailAsync(
|
||||
string directory, byte[] fileBytes, string mime, string? fileName,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
string? mainImagePath = null;
|
||||
string? thumbnailPath = null;
|
||||
|
||||
try
|
||||
{
|
||||
// Optimize main image
|
||||
var mainImageBytes = await OptimizeImageAsync(fileBytes, MainImageMaxWidth, MainImageMaxHeight);
|
||||
mainImagePath = await UploadFileAsync(directory, mainImageBytes, "image/jpeg", fileName, cancellationToken);
|
||||
|
||||
// Create and upload thumbnail
|
||||
var thumbnailBytes = await OptimizeImageAsync(fileBytes, ThumbnailMaxWidth, ThumbnailMaxHeight);
|
||||
var thumbFileName = fileName != null ? $"thumb_{fileName}" : null;
|
||||
thumbnailPath = await UploadFileAsync($"{directory}/Thumbnails", thumbnailBytes, "image/jpeg", thumbFileName, cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error processing and uploading image with thumbnail. Directory: {Directory}", directory);
|
||||
}
|
||||
|
||||
return (mainImagePath, thumbnailPath);
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteFileAsync(long fileId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var request = new DeleteFileInfoRequest { Id = fileId };
|
||||
var response = await _client.DeleteFileInfoAsync(request, cancellationToken: cancellationToken);
|
||||
return response?.Success ?? false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error deleting file from FMS. FileId: {FileId}", fileId);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<byte[]> OptimizeImageAsync(byte[] imageBytes, int maxWidth, int maxHeight)
|
||||
{
|
||||
using var image = Image.Load(imageBytes);
|
||||
|
||||
// Only resize if larger than max dimensions
|
||||
if (image.Width > maxWidth || image.Height > maxHeight)
|
||||
{
|
||||
image.Mutate(x => x.Resize(new ResizeOptions
|
||||
{
|
||||
Size = new Size(maxWidth, maxHeight),
|
||||
Mode = ResizeMode.Max
|
||||
}));
|
||||
}
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
await image.SaveAsJpegAsync(ms, new JpegEncoder { Quality = JpegQuality });
|
||||
return ms.ToArray();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_channel?.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using CMSMicroservice.Application.Common.FileManager;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Formats.Jpeg;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
|
||||
namespace CMSMicroservice.Infrastructure.Services;
|
||||
|
||||
/// <summary>
|
||||
/// فایلمنیجر محلی — فایلها روی دیسک ذخیره میشوند
|
||||
/// مسیر نسبی در دیتابیس ذخیره میشود
|
||||
/// موقع واکشی: فایل از دیسک خوانده و به base64 data-URI تبدیل میشود
|
||||
/// </summary>
|
||||
public sealed class LocalFileManager : IFileManager
|
||||
{
|
||||
private readonly ILogger<LocalFileManager> _logger;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly string _uploadRoot;
|
||||
private readonly string _fmsBaseUrl;
|
||||
|
||||
// ── تنظیمات بهینهسازی تصویر ──
|
||||
private const int MainMaxWidth = 1200;
|
||||
private const int MainMaxHeight = 1200;
|
||||
private const int ThumbMaxWidth = 300;
|
||||
private const int ThumbMaxHeight = 300;
|
||||
private const int JpegQuality = 75;
|
||||
|
||||
public LocalFileManager(IConfiguration configuration, IHttpClientFactory httpClientFactory, ILogger<LocalFileManager> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
|
||||
// مسیر ذخیره فایلها — پیشفرض: پوشه Uploads در کنار WebApi
|
||||
_uploadRoot = configuration["FileStorage:UploadPath"]
|
||||
?? Path.Combine(AppContext.BaseDirectory, "Uploads");
|
||||
|
||||
_fmsBaseUrl = configuration["FMS:Address"]?.TrimEnd('/') ?? "https://dl.afrino.co";
|
||||
|
||||
Directory.CreateDirectory(_uploadRoot);
|
||||
_logger.LogInformation("LocalFileManager initialized — UploadRoot: {Root}", _uploadRoot);
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────
|
||||
// آپلود فایل خام → ذخیره روی دیسک → برگرداندن مسیر نسبی
|
||||
// ────────────────────────────────────────────────────
|
||||
public async Task<UploadedFile> UploadAsync(
|
||||
string directory, byte[] fileBytes, string mime,
|
||||
string? fileName = null, CancellationToken ct = default)
|
||||
{
|
||||
if (fileBytes is not { Length: > 0 })
|
||||
throw new FileUploadException("فایلی برای آپلود ارسال نشده است");
|
||||
|
||||
try
|
||||
{
|
||||
var ext = GetExtension(mime, fileName);
|
||||
var uniqueName = $"{Guid.NewGuid():N}{ext}";
|
||||
var relativePath = Path.Combine(directory, uniqueName).Replace('\\', '/');
|
||||
|
||||
var fullPath = Path.Combine(_uploadRoot, relativePath);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(fullPath)!);
|
||||
|
||||
await File.WriteAllBytesAsync(fullPath, fileBytes, ct);
|
||||
|
||||
_logger.LogInformation(
|
||||
"File saved — Path: {Path}, Size: {Size}KB",
|
||||
relativePath, fileBytes.Length / 1024);
|
||||
|
||||
return new UploadedFile(0, relativePath);
|
||||
}
|
||||
catch (FileUploadException) { throw; }
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "خطا در ذخیره فایل — Directory: {Dir}", directory);
|
||||
throw new FileUploadException($"خطا در ذخیره فایل: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────
|
||||
// آپلود تصویر + بندانگشتی → ذخیره روی دیسک
|
||||
// ────────────────────────────────────────────────────
|
||||
public async Task<UploadedImage> UploadImageAsync(
|
||||
string directory, byte[] fileBytes, string mime,
|
||||
string? fileName = null, CancellationToken ct = default)
|
||||
{
|
||||
if (fileBytes is not { Length: > 0 })
|
||||
throw new FileUploadException("تصویری برای آپلود ارسال نشده است");
|
||||
|
||||
var baseName = Guid.NewGuid().ToString("N");
|
||||
|
||||
// ① بهینهسازی و ذخیره تصویر اصلی
|
||||
var mainBytes = await OptimizeAsync(fileBytes, MainMaxWidth, MainMaxHeight);
|
||||
var mainRelative = Path.Combine(directory, $"{baseName}.jpg").Replace('\\', '/');
|
||||
var mainFull = Path.Combine(_uploadRoot, mainRelative);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(mainFull)!);
|
||||
await File.WriteAllBytesAsync(mainFull, mainBytes, ct);
|
||||
var main = new UploadedFile(0, mainRelative);
|
||||
|
||||
// ② ساخت و ذخیره بندانگشتی
|
||||
var thumbBytes = await OptimizeAsync(fileBytes, ThumbMaxWidth, ThumbMaxHeight);
|
||||
var thumbRelative = Path.Combine(directory, $"{baseName}_thumb.jpg").Replace('\\', '/');
|
||||
var thumbFull = Path.Combine(_uploadRoot, thumbRelative);
|
||||
await File.WriteAllBytesAsync(thumbFull, thumbBytes, ct);
|
||||
var thumb = new UploadedFile(0, thumbRelative);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Image saved — Main: {MainPath} ({MainKB}KB), Thumb: {ThumbPath} ({ThumbKB}KB)",
|
||||
mainRelative, mainBytes.Length / 1024,
|
||||
thumbRelative, thumbBytes.Length / 1024);
|
||||
|
||||
return new UploadedImage(main, thumb);
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────
|
||||
// حذف فایل از دیسک
|
||||
// ────────────────────────────────────────────────────
|
||||
public Task DeleteAsync(long fileId, CancellationToken ct = default)
|
||||
{
|
||||
_logger.LogWarning("DeleteAsync called with fileId={Id} — file deletion by ID not supported in disk mode", fileId);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────
|
||||
// خواندن فایل از دیسک → تبدیل به base64 data-URI
|
||||
// ────────────────────────────────────────────────────
|
||||
public string ResolveImageUrl(string? path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
return string.Empty;
|
||||
|
||||
// اگر از قبل data-URI یا URL مطلق هست، همان را برگردان
|
||||
if (path.StartsWith("data:", StringComparison.OrdinalIgnoreCase) ||
|
||||
path.StartsWith("http://", StringComparison.OrdinalIgnoreCase) ||
|
||||
path.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
|
||||
return path;
|
||||
|
||||
try
|
||||
{
|
||||
var fullPath = Path.Combine(_uploadRoot, path.TrimStart('/'));
|
||||
if (!File.Exists(fullPath))
|
||||
{
|
||||
_logger.LogWarning("Image file not found on disk, trying FMS fallback: {Path}", fullPath);
|
||||
|
||||
// ── FMS Fallback: دانلود از dl.afrino.co و کش محلی (برای مهاجرت) ──
|
||||
if (!TryDownloadFromFms(path.TrimStart('/'), fullPath))
|
||||
return string.Empty;
|
||||
|
||||
_logger.LogInformation("Downloaded and cached from FMS: {Path}", path);
|
||||
}
|
||||
|
||||
var bytes = File.ReadAllBytes(fullPath);
|
||||
var mime = GetMimeFromExtension(Path.GetExtension(fullPath));
|
||||
return $"data:{mime};base64,{Convert.ToBase64String(bytes)}";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error reading image from disk: {Path}", path);
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────
|
||||
// FMS Fallback — دانلود از سرور قدیمی و کش محلی (مهاجرت)
|
||||
// ────────────────────────────────────────────────────
|
||||
private bool TryDownloadFromFms(string relativePath, string localPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
var fmsUrl = $"{_fmsBaseUrl}/{relativePath}";
|
||||
_logger.LogInformation("Attempting FMS download: {Url}", fmsUrl);
|
||||
|
||||
using var client = _httpClientFactory.CreateClient("FMS");
|
||||
using var response = client.Send(new HttpRequestMessage(HttpMethod.Get, fmsUrl),
|
||||
HttpCompletionOption.ResponseHeadersRead);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.LogWarning("FMS returned {Status} for {Url}", response.StatusCode, fmsUrl);
|
||||
return false;
|
||||
}
|
||||
|
||||
// ذخیره روی دیسک
|
||||
var directory = Path.GetDirectoryName(localPath)!;
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
using var responseStream = response.Content.ReadAsStream();
|
||||
using var fileStream = new FileStream(localPath, FileMode.Create, FileAccess.Write, FileShare.None);
|
||||
responseStream.CopyTo(fileStream);
|
||||
|
||||
_logger.LogInformation("Cached FMS file locally: {Path} ({Size} bytes)", relativePath, fileStream.Length);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to download from FMS: {Path}", relativePath);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────
|
||||
// بهینهسازی تصویر (ریسایز + فشردهسازی JPEG)
|
||||
// ────────────────────────────────────────────────────
|
||||
private static async Task<byte[]> OptimizeAsync(byte[] imageBytes, int maxWidth, int maxHeight)
|
||||
{
|
||||
using var image = SixLabors.ImageSharp.Image.Load(imageBytes);
|
||||
|
||||
if (image.Width > maxWidth || image.Height > maxHeight)
|
||||
{
|
||||
image.Mutate(x => x.Resize(new ResizeOptions
|
||||
{
|
||||
Size = new Size(maxWidth, maxHeight),
|
||||
Mode = ResizeMode.Max
|
||||
}));
|
||||
}
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
await image.SaveAsJpegAsync(ms, new JpegEncoder { Quality = JpegQuality });
|
||||
return ms.ToArray();
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────
|
||||
// پسوند فایل از mime type
|
||||
// ────────────────────────────────────────────────────
|
||||
private static string GetExtension(string mime, string? fileName)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(fileName))
|
||||
{
|
||||
var ext = Path.GetExtension(fileName);
|
||||
if (!string.IsNullOrEmpty(ext))
|
||||
return ext.ToLowerInvariant();
|
||||
}
|
||||
|
||||
return mime.ToLowerInvariant() switch
|
||||
{
|
||||
"image/jpeg" or "image/jpg" => ".jpg",
|
||||
"image/png" => ".png",
|
||||
"image/gif" => ".gif",
|
||||
"image/webp" => ".webp",
|
||||
"image/svg+xml" => ".svg",
|
||||
"application/pdf" => ".pdf",
|
||||
_ => ".bin"
|
||||
};
|
||||
}
|
||||
|
||||
private static string GetMimeFromExtension(string extension)
|
||||
{
|
||||
return extension.ToLowerInvariant() switch
|
||||
{
|
||||
".jpg" or ".jpeg" => "image/jpeg",
|
||||
".png" => "image/png",
|
||||
".gif" => "image/gif",
|
||||
".webp" => "image/webp",
|
||||
".svg" => "image/svg+xml",
|
||||
".pdf" => "application/pdf",
|
||||
_ => "application/octet-stream"
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Protobuf.Protos.PYMS;
|
||||
using CMSMicroservice.Protobuf.Protos.PYMS.Transaction;
|
||||
using Grpc.Net.Client;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace CMSMicroservice.Infrastructure.Services.Payment;
|
||||
|
||||
/// <summary>
|
||||
/// پیادهسازی درگاه پرداخت از طریق PYMS (Payment Microservice)
|
||||
/// CMS به جای اتصال مستقیم به ZarinPal، از PYMS استفاده میکند.
|
||||
/// PYMS تراکنشها را ذخیره و با ZarinPal ارتباط برقرار میکند.
|
||||
/// </summary>
|
||||
public class PYMSPaymentService : IPaymentGatewayService, IDisposable
|
||||
{
|
||||
private readonly ILogger<PYMSPaymentService> _logger;
|
||||
private readonly GrpcChannel _channel;
|
||||
private readonly TransactionContract.TransactionContractClient _client;
|
||||
private readonly string _merchantId;
|
||||
private readonly bool _useSandbox;
|
||||
|
||||
public PYMSPaymentService(
|
||||
IConfiguration configuration,
|
||||
ILogger<PYMSPaymentService> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
|
||||
var pymsAddress = configuration["PYMS:Address"]
|
||||
?? throw new InvalidOperationException("PYMS:Address is not configured.");
|
||||
|
||||
_merchantId = configuration["ZarinPal:MerchantId"]
|
||||
?? throw new InvalidOperationException("ZarinPal:MerchantId is not configured.");
|
||||
|
||||
_useSandbox = configuration.GetValue<bool>("ZarinPal:UseSandbox", true);
|
||||
|
||||
// ایجاد کانال gRPC به PYMS
|
||||
_channel = GrpcChannel.ForAddress(pymsAddress, new GrpcChannelOptions
|
||||
{
|
||||
HttpHandler = new SocketsHttpHandler
|
||||
{
|
||||
EnableMultipleHttp2Connections = true,
|
||||
PooledConnectionIdleTimeout = TimeSpan.FromMinutes(5),
|
||||
KeepAlivePingDelay = TimeSpan.FromSeconds(60),
|
||||
KeepAlivePingTimeout = TimeSpan.FromSeconds(30),
|
||||
}
|
||||
});
|
||||
|
||||
_client = new TransactionContract.TransactionContractClient(_channel);
|
||||
|
||||
_logger.LogInformation(
|
||||
"PYMS Payment Service initialized. Address={Address}, Mode={Mode}",
|
||||
pymsAddress, _useSandbox ? "🧪 Sandbox" : "🏦 Production");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// مرحله ۱: ارسال درخواست پرداخت به PYMS
|
||||
/// PYMS تراکنش را ایجاد و URL درگاه را برمیگرداند
|
||||
/// </summary>
|
||||
public async Task<PaymentInitiateResult> InitiatePaymentAsync(
|
||||
PaymentRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
// CMS مبالغ را به تومان نگهداری میکند
|
||||
// PYMS مبلغ را به ریال میخواهد — تبدیل تومان به ریال
|
||||
var amountInRials = (long)(request.Amount * 10);
|
||||
|
||||
var grpcRequest = new PaymentRequestRequest
|
||||
{
|
||||
MerchantId = _merchantId,
|
||||
Amount = amountInRials,
|
||||
CallbackUrl = request.CallbackUrl ?? string.Empty,
|
||||
Description = request.Description ?? string.Empty,
|
||||
OrderId = request.UserId.ToString(),
|
||||
// نوع تراکنش: Sandbox برای تست، Real برای Production
|
||||
Type = _useSandbox ? TransactionTypeEnum.Sandbox : TransactionTypeEnum.Real,
|
||||
Currency = CurrencyEnum.Irt, // تومان
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.Mobile))
|
||||
grpcRequest.Mobile = request.Mobile;
|
||||
|
||||
_logger.LogInformation(
|
||||
"PYMS payment request: Amount={AmountToman} Toman ({AmountRial} Rial), User={UserId}, Sandbox={Sandbox}",
|
||||
request.Amount, amountInRials, request.UserId, _useSandbox);
|
||||
|
||||
var response = await _client.PaymentRequestAsync(grpcRequest, cancellationToken: cancellationToken);
|
||||
|
||||
if (!string.IsNullOrEmpty(response.PaymentGWUrl))
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"PYMS payment initiated successfully: GatewayUrl={Url}",
|
||||
response.PaymentGWUrl);
|
||||
|
||||
// از URL درگاه، Authority را استخراج میکنیم (آخرین بخش URL)
|
||||
var authority = ExtractAuthorityFromUrl(response.PaymentGWUrl);
|
||||
|
||||
return new PaymentInitiateResult
|
||||
{
|
||||
IsSuccess = true,
|
||||
RefId = authority,
|
||||
GatewayUrl = response.PaymentGWUrl
|
||||
};
|
||||
}
|
||||
|
||||
_logger.LogError("PYMS payment request failed: Empty gateway URL returned");
|
||||
|
||||
return new PaymentInitiateResult
|
||||
{
|
||||
IsSuccess = false,
|
||||
ErrorMessage = "خطا در دریافت آدرس درگاه از PYMS"
|
||||
};
|
||||
}
|
||||
catch (Grpc.Core.RpcException ex)
|
||||
{
|
||||
_logger.LogError(ex, "PYMS gRPC error in InitiatePayment: Status={Status}, Detail={Detail}",
|
||||
ex.StatusCode, ex.Status.Detail);
|
||||
|
||||
return new PaymentInitiateResult
|
||||
{
|
||||
IsSuccess = false,
|
||||
ErrorMessage = $"خطا در ارتباط با سرویس پرداخت: {ex.Status.Detail}"
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "PYMS InitiatePayment exception");
|
||||
return new PaymentInitiateResult
|
||||
{
|
||||
IsSuccess = false,
|
||||
ErrorMessage = $"خطا در ارتباط با سرویس پرداخت: {ex.Message}"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// تأیید پرداخت بدون مبلغ — PYMS خودش مبلغ را از تراکنش ذخیرهشده میخواند
|
||||
/// </summary>
|
||||
public async Task<PaymentVerificationResult> VerifyPaymentAsync(
|
||||
string refId,
|
||||
string verificationToken,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await VerifyPaymentInternalAsync(refId, verificationToken, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// تأیید پرداخت با مبلغ — PYMS خودش verify را انجام میدهد
|
||||
/// refId = Authority, verificationToken = Status (OK/NOK)
|
||||
/// </summary>
|
||||
public async Task<PaymentVerificationResult> VerifyPaymentAsync(
|
||||
string refId,
|
||||
string verificationToken,
|
||||
decimal amountInToman,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await VerifyPaymentInternalAsync(refId, verificationToken, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<PaymentVerificationResult> VerifyPaymentInternalAsync(
|
||||
string refId,
|
||||
string verificationToken,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
// اگر کاربر لغو کرده
|
||||
if (!string.Equals(verificationToken, "OK", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_logger.LogWarning("Payment cancelled by user: Authority={Authority}", refId);
|
||||
return new PaymentVerificationResult
|
||||
{
|
||||
IsSuccess = false,
|
||||
RefId = refId,
|
||||
Message = "پرداخت توسط کاربر لغو شد"
|
||||
};
|
||||
}
|
||||
|
||||
var grpcRequest = new PaymentVerificationRequest
|
||||
{
|
||||
Authority = refId,
|
||||
Status = verificationToken
|
||||
};
|
||||
|
||||
_logger.LogInformation("PYMS verify request: Authority={Authority}, Status={Status}",
|
||||
refId, verificationToken);
|
||||
|
||||
var response = await _client.PaymentVerificationAsync(grpcRequest, cancellationToken: cancellationToken);
|
||||
|
||||
if (response.PaymentStatus)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"PYMS payment verified: Id={Id}, RefId={RefId}, OrderId={OrderId}, StatusCode={StatusCode}",
|
||||
response.Id, response.RefId, response.OrderId, response.VerificationStatusCode);
|
||||
|
||||
return new PaymentVerificationResult
|
||||
{
|
||||
IsSuccess = true,
|
||||
RefId = refId,
|
||||
TrackingCode = response.RefId,
|
||||
Amount = 0, // مبلغ از DB خوانده میشود
|
||||
Message = response.Message ?? "تراکنش موفق"
|
||||
};
|
||||
}
|
||||
|
||||
_logger.LogError(
|
||||
"PYMS verify failed: Authority={Authority}, StatusCode={StatusCode}, Message={Message}",
|
||||
refId, response.VerificationStatusCode, response.Message);
|
||||
|
||||
return new PaymentVerificationResult
|
||||
{
|
||||
IsSuccess = false,
|
||||
RefId = refId,
|
||||
Message = response.Message ?? "تأیید پرداخت ناموفق"
|
||||
};
|
||||
}
|
||||
catch (Grpc.Core.RpcException ex)
|
||||
{
|
||||
_logger.LogError(ex, "PYMS gRPC error in VerifyPayment: Status={Status}, Detail={Detail}",
|
||||
ex.StatusCode, ex.Status.Detail);
|
||||
|
||||
return new PaymentVerificationResult
|
||||
{
|
||||
IsSuccess = false,
|
||||
RefId = refId,
|
||||
Message = $"خطا در تأیید تراکنش: {ex.Status.Detail}"
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "PYMS VerifyPayment exception: Authority={Authority}", refId);
|
||||
return new PaymentVerificationResult
|
||||
{
|
||||
IsSuccess = false,
|
||||
RefId = refId,
|
||||
Message = $"خطا در تأیید تراکنش: {ex.Message}"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PYMS فعلاً قابلیت Payout ندارد
|
||||
/// </summary>
|
||||
public Task<PayoutResult> ProcessPayoutAsync(
|
||||
PayoutRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_logger.LogWarning("PYMS does not support direct payout yet.");
|
||||
return Task.FromResult(new PayoutResult
|
||||
{
|
||||
IsSuccess = false,
|
||||
Message = "سرویس پرداخت (PYMS) فعلاً از قابلیت واریز مستقیم پشتیبانی نمیکند",
|
||||
ProcessedAt = DateTime.UtcNow
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// استخراج Authority از URL درگاه
|
||||
/// مثال: https://sandbox.zarinpal.com/pg/StartPay/A00000000000000000000000000123456789 → A00000000000000000000000000123456789
|
||||
/// </summary>
|
||||
private static string ExtractAuthorityFromUrl(string gatewayUrl)
|
||||
{
|
||||
if (string.IsNullOrEmpty(gatewayUrl))
|
||||
return string.Empty;
|
||||
|
||||
// Authority معمولاً آخرین بخش URL است
|
||||
var uri = new Uri(gatewayUrl);
|
||||
var segments = uri.Segments;
|
||||
if (segments.Length > 0)
|
||||
{
|
||||
return segments[^1].TrimEnd('/');
|
||||
}
|
||||
|
||||
return gatewayUrl;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_channel?.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,358 @@
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CMSMicroservice.Infrastructure.Services.Payment;
|
||||
|
||||
/// <summary>
|
||||
/// پیادهسازی درگاه پرداخت زرینپال
|
||||
/// ساپورت Sandbox (تست) و Production
|
||||
/// </summary>
|
||||
public class ZarinPalPaymentService : IPaymentGatewayService
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly ILogger<ZarinPalPaymentService> _logger;
|
||||
private readonly string _merchantId;
|
||||
private readonly bool _useSandbox;
|
||||
|
||||
// آدرسهای Production
|
||||
private const string ProductionApiBase = "https://api.zarinpal.com";
|
||||
private const string ProductionStartPayBase = "https://www.zarinpal.com";
|
||||
|
||||
// آدرسهای Sandbox
|
||||
private const string SandboxApiBase = "https://sandbox.zarinpal.com";
|
||||
private const string SandboxStartPayBase = "https://sandbox.zarinpal.com";
|
||||
|
||||
// مسیرهای API (مشترک)
|
||||
private const string RequestEndpoint = "/pg/v4/payment/request.json";
|
||||
private const string VerifyEndpoint = "/pg/v4/payment/verify.json";
|
||||
private const string StartPayPath = "/pg/StartPay/";
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
|
||||
public ZarinPalPaymentService(
|
||||
HttpClient httpClient,
|
||||
IConfiguration configuration,
|
||||
ILogger<ZarinPalPaymentService> logger)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
|
||||
_merchantId = configuration["ZarinPal:MerchantId"]
|
||||
?? throw new InvalidOperationException("ZarinPal:MerchantId is not configured.");
|
||||
_useSandbox = configuration.GetValue<bool>("ZarinPal:UseSandbox", true);
|
||||
|
||||
var apiBase = _useSandbox ? SandboxApiBase : ProductionApiBase;
|
||||
_httpClient.BaseAddress = new Uri(apiBase);
|
||||
|
||||
_logger.LogInformation("ZarinPal payment service initialized. Mode: {Mode}",
|
||||
_useSandbox ? "🧪 Sandbox" : "🏦 Production");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// مرحله ۱: ارسال درخواست پرداخت به زرینپال و دریافت Authority
|
||||
/// </summary>
|
||||
public async Task<PaymentInitiateResult> InitiatePaymentAsync(
|
||||
PaymentRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
// زرینپال مبلغ را به ریال میخواهد — تبدیل تومان به ریال
|
||||
var amountInRials = (long)(request.Amount * 10);
|
||||
|
||||
var zarinPalRequest = new ZarinPalPaymentRequest
|
||||
{
|
||||
MerchantId = _merchantId,
|
||||
Amount = amountInRials,
|
||||
Description = request.Description,
|
||||
CallbackUrl = request.CallbackUrl,
|
||||
Metadata = new ZarinPalMetadata
|
||||
{
|
||||
Mobile = string.IsNullOrWhiteSpace(request.Mobile) ? null : request.Mobile
|
||||
}
|
||||
};
|
||||
|
||||
var jsonContent = JsonSerializer.Serialize(zarinPalRequest, JsonOptions);
|
||||
var content = new StringContent(jsonContent, Encoding.UTF8, "application/json");
|
||||
|
||||
_logger.LogInformation(
|
||||
"ZarinPal payment request: Amount={AmountToman} Toman ({AmountRial} Rial), User={UserId}, Sandbox={Sandbox}",
|
||||
request.Amount, amountInRials, request.UserId, _useSandbox);
|
||||
|
||||
var response = await _httpClient.PostAsync(RequestEndpoint, content, cancellationToken);
|
||||
var responseBody = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
|
||||
_logger.LogDebug("ZarinPal request response: {StatusCode} - {Body}",
|
||||
response.StatusCode, responseBody);
|
||||
|
||||
var result = JsonSerializer.Deserialize<ZarinPalResponse>(responseBody, JsonOptions);
|
||||
|
||||
if (result?.Data?.Code == 100 && !string.IsNullOrEmpty(result.Data.Authority))
|
||||
{
|
||||
var startPayBase = _useSandbox ? SandboxStartPayBase : ProductionStartPayBase;
|
||||
var gatewayUrl = $"{startPayBase}{StartPayPath}{result.Data.Authority}";
|
||||
|
||||
_logger.LogInformation(
|
||||
"ZarinPal payment initiated successfully: Authority={Authority}, GatewayUrl={Url}",
|
||||
result.Data.Authority, gatewayUrl);
|
||||
|
||||
return new PaymentInitiateResult
|
||||
{
|
||||
IsSuccess = true,
|
||||
RefId = result.Data.Authority,
|
||||
GatewayUrl = gatewayUrl
|
||||
};
|
||||
}
|
||||
|
||||
// خطا
|
||||
var errorCode = result?.Errors?.Code ?? result?.Data?.Code ?? -1;
|
||||
var errorMessage = result?.Errors?.Message ?? "خطای ناشناخته از زرینپال";
|
||||
|
||||
_logger.LogError(
|
||||
"ZarinPal payment request failed: Code={Code}, Message={Message}",
|
||||
errorCode, errorMessage);
|
||||
|
||||
return new PaymentInitiateResult
|
||||
{
|
||||
IsSuccess = false,
|
||||
ErrorMessage = $"خطای درگاه زرینپال (کد {errorCode}): {errorMessage}"
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "ZarinPal InitiatePayment exception");
|
||||
return new PaymentInitiateResult
|
||||
{
|
||||
IsSuccess = false,
|
||||
ErrorMessage = $"خطا در ارتباط با درگاه زرینپال: {ex.Message}"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// تأیید پرداخت بدون مبلغ — برای سازگاری با اینترفیس.
|
||||
/// ⚠ زرینپال مبلغ را در Verify نیاز دارد. از overload با amount استفاده کنید.
|
||||
/// </summary>
|
||||
public Task<PaymentVerificationResult> VerifyPaymentAsync(
|
||||
string refId,
|
||||
string verificationToken,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_logger.LogWarning("ZarinPal VerifyPaymentAsync called without amount — verification may fail!");
|
||||
return VerifyPaymentWithAmountAsync(refId, verificationToken, 0, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// تأیید پرداخت با مبلغ — نسخه اصلی برای زرینپال
|
||||
/// refId = Authority، verificationToken = Status (OK/NOK)، amountInToman = مبلغ به تومان
|
||||
/// </summary>
|
||||
public Task<PaymentVerificationResult> VerifyPaymentAsync(
|
||||
string refId,
|
||||
string verificationToken,
|
||||
decimal amountInToman,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return VerifyPaymentWithAmountAsync(refId, verificationToken, amountInToman, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<PaymentVerificationResult> VerifyPaymentWithAmountAsync(
|
||||
string refId,
|
||||
string verificationToken,
|
||||
decimal amountInToman,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
// verificationToken باید "OK" باشد — در غیر اینصورت کاربر لغو کرده
|
||||
if (!string.Equals(verificationToken, "OK", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_logger.LogWarning("ZarinPal payment cancelled by user: Authority={Authority}", refId);
|
||||
return new PaymentVerificationResult
|
||||
{
|
||||
IsSuccess = false,
|
||||
RefId = refId,
|
||||
Message = "پرداخت توسط کاربر لغو شد"
|
||||
};
|
||||
}
|
||||
|
||||
// تبدیل تومان → ریال (×۱۰)
|
||||
var amountInRials = (long)(amountInToman * 10);
|
||||
|
||||
var verifyRequest = new ZarinPalVerifyRequest
|
||||
{
|
||||
MerchantId = _merchantId,
|
||||
Authority = refId,
|
||||
Amount = amountInRials
|
||||
};
|
||||
|
||||
var jsonContent = JsonSerializer.Serialize(verifyRequest, JsonOptions);
|
||||
var content = new StringContent(jsonContent, Encoding.UTF8, "application/json");
|
||||
|
||||
_logger.LogInformation("ZarinPal verify request: Authority={Authority}, Amount={Amount} Rial",
|
||||
refId, amountInRials);
|
||||
|
||||
var response = await _httpClient.PostAsync(VerifyEndpoint, content, cancellationToken);
|
||||
var responseBody = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
|
||||
_logger.LogDebug("ZarinPal verify response: {StatusCode} - {Body}",
|
||||
response.StatusCode, responseBody);
|
||||
|
||||
var result = JsonSerializer.Deserialize<ZarinPalResponse>(responseBody, JsonOptions);
|
||||
|
||||
// code 100 = موفق | code 101 = قبلاً تأیید شده
|
||||
if (result?.Data?.Code is 100 or 101)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"ZarinPal payment verified: Authority={Authority}, RefId={RefId}, CardPan={CardPan}",
|
||||
refId, result.Data.RefId, result.Data.CardPan);
|
||||
|
||||
return new PaymentVerificationResult
|
||||
{
|
||||
IsSuccess = true,
|
||||
RefId = refId,
|
||||
TrackingCode = result.Data.RefId?.ToString(),
|
||||
Amount = (result.Data.Amount ?? 0) / 10m, // ریال → تومان
|
||||
Message = result.Data.Code == 101
|
||||
? "تراکنش قبلاً تأیید شده"
|
||||
: "تراکنش موفق"
|
||||
};
|
||||
}
|
||||
|
||||
var errorCode = result?.Errors?.Code ?? result?.Data?.Code ?? -1;
|
||||
var errorMessage = result?.Errors?.Message ?? "تأیید تراکنش ناموفق";
|
||||
|
||||
_logger.LogError(
|
||||
"ZarinPal verify failed: Authority={Authority}, Code={Code}, Message={Message}",
|
||||
refId, errorCode, errorMessage);
|
||||
|
||||
return new PaymentVerificationResult
|
||||
{
|
||||
IsSuccess = false,
|
||||
RefId = refId,
|
||||
Message = $"تأیید پرداخت ناموفق (کد {errorCode}): {errorMessage}"
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "ZarinPal VerifyPayment exception: Authority={Authority}", refId);
|
||||
return new PaymentVerificationResult
|
||||
{
|
||||
IsSuccess = false,
|
||||
RefId = refId,
|
||||
Message = $"خطا در تأیید تراکنش: {ex.Message}"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// زرینپال Payout مستقیم ندارد — این متد NotSupported برمیگرداند
|
||||
/// برای Payout باید از سرویس دیگری (مثل دایا) استفاده شود
|
||||
/// </summary>
|
||||
public Task<PayoutResult> ProcessPayoutAsync(
|
||||
PayoutRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_logger.LogWarning("ZarinPal does not support direct payout. Use a different provider for payouts.");
|
||||
return Task.FromResult(new PayoutResult
|
||||
{
|
||||
IsSuccess = false,
|
||||
Message = "درگاه زرینپال از قابلیت واریز مستقیم پشتیبانی نمیکند",
|
||||
ProcessedAt = DateTime.UtcNow
|
||||
});
|
||||
}
|
||||
|
||||
// ── ZarinPal Request/Response DTOs ──
|
||||
|
||||
private class ZarinPalPaymentRequest
|
||||
{
|
||||
public string MerchantId { get; set; } = string.Empty;
|
||||
public long Amount { get; set; }
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public string CallbackUrl { get; set; } = string.Empty;
|
||||
public ZarinPalMetadata? Metadata { get; set; }
|
||||
}
|
||||
|
||||
private class ZarinPalMetadata
|
||||
{
|
||||
public string? Mobile { get; set; }
|
||||
public string? Email { get; set; }
|
||||
}
|
||||
|
||||
private class ZarinPalVerifyRequest
|
||||
{
|
||||
public string MerchantId { get; set; } = string.Empty;
|
||||
public long Amount { get; set; }
|
||||
public string Authority { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
private class ZarinPalResponse
|
||||
{
|
||||
public ZarinPalResponseData? Data { get; set; }
|
||||
|
||||
[JsonConverter(typeof(ZarinPalErrorsConverter))]
|
||||
public ZarinPalResponseErrors? Errors { get; set; }
|
||||
}
|
||||
|
||||
private class ZarinPalResponseData
|
||||
{
|
||||
public int? Code { get; set; }
|
||||
public string? Message { get; set; }
|
||||
public string? Authority { get; set; }
|
||||
public long? RefId { get; set; }
|
||||
public long? Amount { get; set; }
|
||||
public string? CardPan { get; set; }
|
||||
public string? CardHash { get; set; }
|
||||
public string? FeeType { get; set; }
|
||||
public long? Fee { get; set; }
|
||||
}
|
||||
|
||||
private class ZarinPalResponseErrors
|
||||
{
|
||||
public int? Code { get; set; }
|
||||
public string? Message { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ZarinPal returns errors as [] (empty array) when no error, or as {...} object when there's an error.
|
||||
/// This converter handles both cases.
|
||||
/// </summary>
|
||||
private class ZarinPalErrorsConverter : JsonConverter<ZarinPalResponseErrors?>
|
||||
{
|
||||
public override ZarinPalResponseErrors? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.StartArray)
|
||||
{
|
||||
// Skip the empty array []
|
||||
while (reader.Read() && reader.TokenType != JsonTokenType.EndArray) { }
|
||||
return null;
|
||||
}
|
||||
|
||||
if (reader.TokenType == JsonTokenType.StartObject)
|
||||
{
|
||||
return JsonSerializer.Deserialize<ZarinPalResponseErrors>(ref reader);
|
||||
}
|
||||
|
||||
if (reader.TokenType == JsonTokenType.Null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
reader.Skip();
|
||||
return null;
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, ZarinPalResponseErrors? value, JsonSerializerOptions options)
|
||||
{
|
||||
JsonSerializer.Serialize(writer, value, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user