Files
ERP-core/Backend/ERPCore/Program.cs
T

346 lines
19 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Text.Json.Serialization;
using ERPCore.Infra.Auth;
using ERPCore.Infra.Auth.AuthHex;
using ERPCore.Infra.Gl;
using ERPCore.Infra.Persistence;
using ERPCore.Infra.Storage;
using ERPCore.Infra.UoW;
using ERPCore.Repositories;
using ERPCore.Repositories.Interfaces;
using ERPCore.Services;
using ERPCore.Services.Interfaces;
using ERPCore.Services.Auth;
using ERPCore.Services.Hrm;
using ERPCore.Services.Production;
using ERPCore.Services.Stock;
using ERPCore.System.Errors;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.EntityFrameworkCore;
using Microsoft.OpenApi;
using Serilog;
var builder = WebApplication.CreateBuilder(args);
// Serilog (file sink) — reads levels from configuration, plus a rolling daily file.
builder.Host.UseSerilog((ctx, cfg) => cfg
.ReadFrom.Configuration(ctx.Configuration)
.WriteTo.File("logs/erpcore-.log", rollingInterval: RollingInterval.Day));
// Controllers + JSON: serialize enums as their string names (docs/11 §8, camelCase).
builder.Services.AddControllers(o =>
{
o.Filters.Add<ERPCore.Infra.Filters.LowStockAlertFilter>();
// CSRF double-submit check on every mutating, cookie-authenticated endpoint (docs/02-SECURITY.md §B.2).
o.Filters.Add<ERPCore.Infra.Auth.CsrfProtectionFilter>();
})
.AddJsonOptions(o => o.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()));
// EF Core + MariaDB (Pomelo). The server version is pinned explicitly rather than using
// ServerVersion.AutoDetect: AutoDetect opens a connection, which would make design-time
// commands (`dotnet ef migrations add`) and startup depend on the database being reachable.
// MariaDbServerVersion — not MySqlServerVersion — because the target is MariaDB 10.11, and
// Pomelo changes its SQL translation based on which it is given.
// IsNullOrWhiteSpace, not just null: appsettings.json ships the key with an empty value so the
// shape is self-documenting, and each environment supplies the real one. A blank must fail here
// with this message rather than reaching UseMySql, which would report an opaque parse error.
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
if (string.IsNullOrWhiteSpace(connectionString))
throw new InvalidOperationException(
"ConnectionStrings:DefaultConnection is not configured. Set it in appsettings.<Environment>.json "
+ "or as the ConnectionStrings__DefaultConnection environment variable.");
// Optional override for a different local server, e.g. "8.0.44-mysql" in user-secrets
// (Database:ServerVersion) — Pomelo otherwise emits MariaDB-only SQL (INSERT … RETURNING) that
// MySQL rejects. Unset everywhere else, so the MariaDB 10.11 default above still applies.
var serverVersionOverride = builder.Configuration["Database:ServerVersion"];
ServerVersion serverVersion = string.IsNullOrWhiteSpace(serverVersionOverride)
? new MariaDbServerVersion(new Version(10, 11, 15))
: ServerVersion.Parse(serverVersionOverride);
builder.Services.AddDbContext<ErpDbContext>(o =>
{
o.UseMySql(connectionString, serverVersion);
});
// ProblemDetails (RFC 7807) + domain-exception mapping
builder.Services.AddProblemDetails();
builder.Services.AddExceptionHandler<DomainExceptionHandler>();
// Auth: validate external AuthHex RS256 tokens + ERP door policy (docs/10 A.4)
builder.Services.AddErpJwtAuth(builder.Configuration);
// Per-endpoint RBAC (docs/02-SECURITY.md AR-01) — [Authorize(Policy = NavPolicies.For("..."))].
builder.Services.AddMemoryCache();
builder.Services.AddScoped<INavPermissionService, NavPermissionService>();
builder.Services.AddSingleton<IAuthorizationPolicyProvider, NavPolicyProvider>();
builder.Services.AddScoped<IAuthorizationHandler, NavAuthorizationHandler>();
// AuthController proxy → AuthHex (docs/11 §2.0)
builder.Services.AddHttpClient<IAuthHexClient, AuthHexClient>(c =>
{
var baseUrl = builder.Configuration["AuthHex:BaseUrl"]
?? throw new InvalidOperationException("AuthHex:BaseUrl is not configured.");
c.BaseAddress = new Uri(baseUrl);
});
builder.Services.AddScoped<IAuthUserService, AuthUserService>();
builder.Services.AddScoped<IAuthRecoveryService, AuthRecoveryService>();
builder.Services.AddScoped<IAuthAltService, AuthAltService>();
// General Ledger service proxy → external GL microservice (docs/12-GENERAL-LEDGER-INTEGRATION.md)
builder.Services.AddHttpClient<IGeneralLedgerClient, GeneralLedgerClient>(c =>
{
var baseUrl = builder.Configuration["GeneralLedgerService:BaseUrl"];
if (string.IsNullOrWhiteSpace(baseUrl))
throw new InvalidOperationException("GeneralLedgerService:BaseUrl is not configured.");
// GeneralLedgerClient builds *relative* URIs ("journal-entries", "accounts", …), so the base
// address must keep its full path — including /api/v1/ — and end in '/'. Without the trailing
// slash, Uri resolution replaces the last segment and every call lands on /api/journal-entries.
if (!baseUrl.EndsWith('/')) baseUrl += "/";
c.BaseAddress = new Uri(baseUrl);
});
builder.Services.AddScoped<IGeneralLedgerService, GeneralLedgerService>();
// Fail fast on a misconfigured chart of accounts: every Gl*Code in configuration must exist in GL,
// otherwise the first user to touch that flow eats a 404 mid-transaction.
builder.Services.AddHostedService<GlAccountCodeValidator>();
// Current-user (audit actor). AuthHex has no sub/nameid → a claims transformation
// JIT-provisions a local shadow user and injects the local `int` id as `nameid`.
builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<ICurrentUser, CurrentUser>();
builder.Services.AddScoped<ICurrentBranchContext, CurrentBranchContext>();
builder.Services.AddScoped<IClaimsTransformation, ShadowUserClaimsTransformation>();
// Unit of work + generic repository base
builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();
builder.Services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
// Aggregate-specific repositories (bespoke queries on top of the generic base)
builder.Services.AddScoped<IBrandRepository, BrandRepository>();
builder.Services.AddScoped<IBundleSaleRepository, BundleSaleRepository>();
builder.Services.AddScoped<ICashInHandRepository, CashInHandRepository>();
builder.Services.AddScoped<ICategoryRepository, CategoryRepository>();
builder.Services.AddScoped<ICountRepository, CountRepository>();
builder.Services.AddScoped<ICustomerRepository, CustomerRepository>();
builder.Services.AddScoped<IDayEndReportRepository, DayEndReportRepository>();
builder.Services.AddScoped<IExpensePaymentRepository, ExpensePaymentRepository>();
builder.Services.AddScoped<IExpenseRepository, ExpenseRepository>();
builder.Services.AddScoped<IExpenseSetupRepository, ExpenseSetupRepository>();
builder.Services.AddScoped<IGrnRepository, GrnRepository>();
builder.Services.AddScoped<IItemImageRepository, ItemImageRepository>();
builder.Services.AddScoped<IItemRepository, ItemRepository>();
builder.Services.AddScoped<IItemTypeRepository, ItemTypeRepository>();
builder.Services.AddScoped<INumberSequenceRepository, NumberSequenceRepository>();
builder.Services.AddScoped<IOpeningStockRepository, OpeningStockRepository>();
builder.Services.AddScoped<IPriceAdjustmentRepository, PriceAdjustmentRepository>();
builder.Services.AddScoped<IProcurementConfigRepository, ProcurementConfigRepository>();
builder.Services.AddScoped<IProductConfigRepository, ProductConfigRepository>();
builder.Services.AddScoped<IPurchaseInvoicePaymentRepository, PurchaseInvoicePaymentRepository>();
builder.Services.AddScoped<IPurchaseInvoiceRepository, PurchaseInvoiceRepository>();
builder.Services.AddScoped<IPurchaseOrderRepository, PurchaseOrderRepository>();
builder.Services.AddScoped<IPurchaseReturnRepository, PurchaseReturnRepository>();
// Master-data services (docs/11 §2)
builder.Services.AddScoped<ICustomerService, CustomerService>();
builder.Services.AddScoped<IItemService, ItemService>();
builder.Services.AddScoped<IItemImageService, ItemImageService>();
builder.Services.AddScoped<IWarrantyPeriodService, WarrantyPeriodService>();
builder.Services.AddScoped<IUomService, UomService>();
builder.Services.AddScoped<ICategoryService, CategoryService>();
builder.Services.AddScoped<IBrandService, BrandService>();
builder.Services.AddScoped<ITaxRateService, TaxRateService>();
builder.Services.AddScoped<IItemTypeService, ItemTypeService>();
//builder.Services.AddScoped<ICompanyProfileService, CompanyProfileService>();
builder.Services.AddScoped<IProductConfigService, ProductConfigService>();
builder.Services.AddScoped<IBranchConfigService, BranchConfigService>();
builder.Services.AddScoped<ISalesConfigService, SalesConfigService>();
builder.Services.AddScoped<IGrnConfigService, GrnConfigService>();
builder.Services.AddScoped<IProcurementConfigService, ProcurementConfigService>();
builder.Services.AddScoped<IAccessConfigService, AccessConfigService>();
// Day-end owner report (docs/DAY_END_OWNER_REPORT_PLAN.md). Messages go through AuthHex, which
// holds the SMTP / SMS credentials; Messaging:Provider = "Log" writes them to the log instead.
builder.Services.AddScoped<ERPCore.Services.OwnerReport.OwnerReportContentBuilder>();
builder.Services.AddScoped<ERPCore.Services.OwnerReport.IOwnerReportService, ERPCore.Services.OwnerReport.OwnerReportService>();
if (string.Equals(builder.Configuration["Messaging:Provider"], "Log", StringComparison.OrdinalIgnoreCase))
builder.Services.AddScoped<ERPCore.Infra.Messaging.IMessageSender, ERPCore.Infra.Messaging.LogOnlyMessageSender>();
else
builder.Services.AddScoped<ERPCore.Infra.Messaging.IMessageSender, ERPCore.Infra.Messaging.AuthHexMessageSender>();
builder.Services.AddHostedService<ERPCore.Services.OwnerReport.OwnerReportJob>();
builder.Services.AddScoped<IOpeningBalanceService, OpeningBalanceService>();
builder.Services.AddScoped<IOpeningStockService, OpeningStockService>();
builder.Services.AddScoped<IGlAccountConfigService, GlAccountConfigService>();
builder.Services.AddScoped<IBranchAccessService, BranchAccessService>();
builder.Services.AddSingleton<INotificationModuleCatalog, NotificationModuleCatalog>();
builder.Services.AddScoped<INotificationConfigService, NotificationConfigService>();
builder.Services.AddScoped<INotificationService, NotificationService>();
builder.Services.AddScoped<IGlobalSearchService, GlobalSearchService>();
builder.Services.AddScoped<IVendorService, VendorService>();
builder.Services.AddScoped<IWarehouseService, WarehouseService>();
builder.Services.AddScoped<ISupportTicketService, SupportTicketService>();
// RBAC / sidebar: Role (shadow of AuthHex) + Permission assignment + user management
builder.Services.AddScoped<IRoleService, RoleService>();
builder.Services.AddScoped<IUserManagementService, UserManagementService>();
// Cross-cutting + procurement services (docs/11 §3)
builder.Services.AddScoped<INumberSequenceService, NumberSequenceService>();
builder.Services.AddScoped<IRequisitionService, RequisitionService>();
builder.Services.AddScoped<IRfqService, RfqService>();
builder.Services.AddScoped<IPurchaseOrderService, PurchaseOrderService>();
// Stock core + goods receipt (docs/11 §4–5)
builder.Services.AddScoped<IItemMeasure, ItemMeasure>();
builder.Services.AddScoped<IFifoCostingService, FifoCostingService>();
builder.Services.AddScoped<IStockService, StockService>();
builder.Services.AddScoped<IGrnService, GrnService>();
// Purchase invoices (vendor invoices consolidating Confirmed GRNs)
builder.Services.AddScoped<IPurchaseInvoiceService, PurchaseInvoiceService>();
builder.Services.AddScoped<IPurchaseInvoicePaymentService, PurchaseInvoicePaymentService>();
// Accounts: expenses (docs/EXPENSE_MODULE_PLAN.md). ExpenseService is also registered as itself:
// ExpensePaymentService reuses its post step so Post & Pay is one transaction.
builder.Services.AddScoped<IExpenseSetupService, ExpenseSetupService>();
builder.Services.AddScoped<ExpenseService>();
builder.Services.AddScoped<IExpenseService>(sp => sp.GetRequiredService<ExpenseService>());
builder.Services.AddScoped<IExpensePaymentService, ExpensePaymentService>();
// Accounts: bank deposits of till cash (docs/BANK_DEPOSIT_MODULE_PLAN.md). The business-day clock
// is shared with the Day-End Sales Report so both agree on what "today" is.
builder.Services.AddSingleton<BusinessDayClock>();
builder.Services.AddScoped<ICashInHandService, CashInHandService>();
builder.Services.AddScoped<IBankDepositService, BankDepositService>();
// Sales (Phase 1)
builder.Services.AddScoped<ISalesPricingService, SalesPricingService>();
builder.Services.AddScoped<ISalesDomainService, SalesDomainService>();
builder.Services.AddScoped<ISalesPostingService, SalesPostingService>();
builder.Services.AddScoped<ISalesDocumentWorkflowService, SalesDocumentWorkflowService>();
builder.Services.AddScoped<ISalesMappingService, SalesMappingService>();
builder.Services.AddScoped<ISalesInvoiceService, SalesInvoiceService>();
builder.Services.AddScoped<ISalesInvoicePaymentService, SalesInvoicePaymentService>();
builder.Services.AddScoped<ISalesSlipService, SalesSlipService>();
builder.Services.AddScoped<IBundleSaleService, BundleSaleService>();
builder.Services.AddScoped<ISalesPromotionSuggestionService, SalesPromotionSuggestionService>();
builder.Services.AddScoped<ISalesReportService, SalesReportService>();
builder.Services.AddScoped<ISalesReturnService, SalesReturnService>();
builder.Services.AddScoped<ISalesDayEndService, SalesDayEndService>();
builder.Services.AddScoped<IDayEndReportService, DayEndReportService>();
// Stock transactions + reference data (docs/11 §5–6)
builder.Services.AddScoped<IStockMutator, StockMutator>();
builder.Services.AddScoped<ISerialTrackingService, SerialTrackingService>();
builder.Services.AddScoped<IItemTrackingService, ItemTrackingService>();
builder.Services.AddScoped<IReasonCodeService, ReasonCodeService>();
// AdjustmentService is also registered as itself: CountService submits its variances through it.
builder.Services.AddScoped<AdjustmentService>();
builder.Services.AddScoped<IAdjustmentService>(sp => sp.GetRequiredService<AdjustmentService>());
builder.Services.AddScoped<IApprovalService, ApprovalService>();
builder.Services.AddScoped<IPriceAdjustmentService, PriceAdjustmentService>();
builder.Services.AddScoped<ApprovalInboxService>();
builder.Services.AddScoped<ITransferService, TransferService>();
builder.Services.AddScoped<ICountService, CountService>();
builder.Services.AddScoped<IReorderService, ReorderService>();
// Low-stock alerts: FIFO issues record into the tracker; the global LowStockAlertFilter checks
// them against reorder points after each successful request and raises "stock.low".
builder.Services.AddScoped<ERPCore.Services.Stock.LowStockTracker>();
builder.Services.AddScoped<ERPCore.Services.Stock.LowStockAlertService>();
builder.Services.AddScoped<ERPCore.Infra.Filters.LowStockAlertFilter>();
builder.Services.AddScoped<IPurchaseReturnService, PurchaseReturnService>();
// Cross-cutting: audit trail + GL-ready journal read access (docs/11 §6 / FR-X-02, FR-STK-13)
builder.Services.AddScoped<IAuditService, AuditService>();
// Dashboard aggregate stats (cross-domain read: stock, GRN, procurement)
builder.Services.AddScoped<IDashboardService, DashboardService>();
builder.Services.AddScoped<IDashboardChartsService, DashboardChartsService>();
// HRM (docs/13-BACKEND-HRM-API.md): org masters, employee core, staff documents
builder.Services.AddSingleton<IFileStorageService, LocalFileStorageService>();
builder.Services.AddScoped<IBranchService, BranchService>();
builder.Services.AddScoped<IDepartmentService, DepartmentService>();
builder.Services.AddScoped<IDesignationService, DesignationService>();
builder.Services.AddScoped<IEmploymentTypeService, EmploymentTypeService>();
builder.Services.AddScoped<IWorkShiftService, WorkShiftService>();
builder.Services.AddScoped<IEmployeeUserLinkService, EmployeeUserLinkService>();
builder.Services.AddScoped<IEmployeeService, EmployeeService>();
builder.Services.AddScoped<IHrDocumentTypeService, HrDocumentTypeService>();
builder.Services.AddScoped<IEmployeeDocumentService, EmployeeDocumentService>();
// HRM: Leave (docs/13-BACKEND-HRM-API.md §5) — LeaveType/LeaveBalance built before
// LeaveRequest since approval increments balances; Attendance depends on LeaveRequest.
builder.Services.AddScoped<ILeaveTypeService, LeaveTypeService>();
builder.Services.AddScoped<ILeaveBalanceService, LeaveBalanceService>();
builder.Services.AddScoped<ILeaveRequestService, LeaveRequestService>();
// HRM: Attendance (docs/13-BACKEND-HRM-API.md §4)
builder.Services.AddScoped<IAttendanceComputationService, AttendanceComputationService>();
builder.Services.AddScoped<IAttendanceUploadService, AttendanceUploadService>();
// HRM: Payroll (docs/13-BACKEND-HRM-API.md §6) — masters/settings before the
// calculation service, which composes them; PayrollRunService orchestrates last.
builder.Services.AddScoped<ISalaryComponentService, SalaryComponentService>();
builder.Services.AddScoped<IEmployeeSalaryStructureService, EmployeeSalaryStructureService>();
builder.Services.AddScoped<IEmployeeLoanService, EmployeeLoanService>();
builder.Services.AddScoped<IPayrollStatutorySettingService, PayrollStatutorySettingService>();
builder.Services.AddScoped<ITaxSlabService, TaxSlabService>();
builder.Services.AddScoped<IPayrollCalculationService, PayrollCalculationService>();
builder.Services.AddScoped<IPayrollRunService, PayrollRunService>();
builder.Services.AddScoped<IPayslipService, PayslipService>();
// HRM: Reports (docs/13-BACKEND-HRM-API.md §6) — read-only, no new entities
builder.Services.AddScoped<IHrReportService, HrReportService>();
// Manufacturing / Production Lines (docs/30-BACKEND-PHASE2.md Part A). Templates stand
// alone; runs consume FifoCostingService for all stock movement. ProductionGraphValidator
// is deliberately unregistered — it is a pure static algorithm, not an injected service.
builder.Services.AddScoped<IProductionTemplateService, ProductionTemplateService>();
builder.Services.AddScoped<IProductionRunService, ProductionRunService>();
// Health checks (EF Core DB)
builder.Services.AddHealthChecks().AddDbContextCheck<ErpDbContext>();
// Swagger / OpenAPI (Swashbuckle v10 → OpenAPI 3.1)
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(o =>
{
o.SwaggerDoc("v1", new OpenApiInfo { Title = "ERPCore API", Version = "v1" });
o.CustomSchemaIds(t => t.FullName!.Replace("+", "."));
});
var app = builder.Build();
// Seed configurable reference data (reason codes) idempotently at startup.
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<ErpDbContext>();
// await EnsureMigrationBaselineAsync(db);
await db.Database.MigrateAsync();
try
{
await DataSeeder.SeedAsync(db, builder.Configuration);
}
catch (Exception ex)
{
throw new InvalidOperationException("Database migration succeeded, but startup seeding failed.", ex);
}
}
app.UseSerilogRequestLogging();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseExceptionHandler(); // -> DomainExceptionHandler / ProblemDetails
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.MapHealthChecks("/health");
app.Run();