Initial commit
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
using PowderCoating.Core.Enums;
|
||||
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Chart of Accounts entry. Supports a flat or one-level hierarchy via ParentAccountId.
|
||||
/// </summary>
|
||||
public class Account : BaseEntity
|
||||
{
|
||||
public string AccountNumber { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public AccountType AccountType { get; set; }
|
||||
public AccountSubType AccountSubType { get; set; }
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>Nullable FK for sub-accounts (one level deep).</summary>
|
||||
public int? ParentAccountId { get; set; }
|
||||
|
||||
/// <summary>System accounts cannot be deleted (seeded defaults).</summary>
|
||||
public bool IsSystem { get; set; } = false;
|
||||
public bool IsActive { get; set; } = true;
|
||||
|
||||
/// <summary>Starting balance when the account was first set up in this system.</summary>
|
||||
public decimal OpeningBalance { get; set; } = 0;
|
||||
/// <summary>The date the opening balance is as-of. Null means it pre-dates all transactions.</summary>
|
||||
public DateTime? OpeningBalanceDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Denormalized running balance kept in sync with each transaction. Positive = normal-balance direction
|
||||
/// (debit-normal for Assets/Expenses/COGS; credit-normal for Liabilities/Equity/Revenue).
|
||||
/// Use AccountsController.RecalculateBalances to rebuild from scratch if needed.
|
||||
/// </summary>
|
||||
public decimal CurrentBalance { get; set; } = 0;
|
||||
|
||||
// Navigation
|
||||
public virtual Account? ParentAccount { get; set; }
|
||||
public virtual ICollection<Account> SubAccounts { get; set; } = new List<Account>();
|
||||
public virtual ICollection<BillLineItem> BillLineItems { get; set; } = new List<BillLineItem>();
|
||||
public virtual ICollection<Bill> Bills { get; set; } = new List<Bill>();
|
||||
public virtual ICollection<BillPayment> BillPayments { get; set; } = new List<BillPayment>();
|
||||
public virtual ICollection<Expense> Expenses { get; set; } = new List<Expense>();
|
||||
public virtual ICollection<Expense> ExpensePaymentAccounts { get; set; } = new List<Expense>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Vendor bill (accounts payable). Represents money owed to a supplier.
|
||||
/// </summary>
|
||||
public class Bill : BaseEntity
|
||||
{
|
||||
public string BillNumber { get; set; } = string.Empty;
|
||||
/// <summary>Vendor's own invoice/reference number.</summary>
|
||||
public string? VendorInvoiceNumber { get; set; }
|
||||
|
||||
public int VendorId { get; set; }
|
||||
/// <summary>Which AP account this bill posts to (default: Accounts Payable 2000).</summary>
|
||||
public int APAccountId { get; set; }
|
||||
|
||||
public DateTime BillDate { get; set; } = DateTime.UtcNow;
|
||||
public DateTime? DueDate { get; set; }
|
||||
|
||||
public BillStatus Status { get; set; } = BillStatus.Draft;
|
||||
public string? Terms { get; set; }
|
||||
public string? Memo { get; set; }
|
||||
|
||||
// Financials
|
||||
public decimal SubTotal { get; set; }
|
||||
public decimal TaxPercent { get; set; }
|
||||
public decimal TaxAmount { get; set; }
|
||||
public decimal Total { get; set; }
|
||||
public decimal AmountPaid { get; set; }
|
||||
[System.ComponentModel.DataAnnotations.Schema.NotMapped]
|
||||
public decimal BalanceDue => Total - AmountPaid;
|
||||
|
||||
/// <summary>Blob path to an attached receipt/invoice document (PDF or image).</summary>
|
||||
public string? ReceiptFilePath { get; set; }
|
||||
|
||||
// Navigation
|
||||
public virtual Vendor Vendor { get; set; } = null!;
|
||||
public virtual Account APAccount { get; set; } = null!;
|
||||
public virtual ICollection<BillLineItem> LineItems { get; set; } = new List<BillLineItem>();
|
||||
public virtual ICollection<BillPayment> Payments { get; set; } = new List<BillPayment>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A single line on a vendor bill, posted to an expense or asset account.
|
||||
/// </summary>
|
||||
public class BillLineItem : BaseEntity
|
||||
{
|
||||
public int BillId { get; set; }
|
||||
/// <summary>Expense/asset account this line item is categorized under. Nullable for QB-imported bills where account is unknown.</summary>
|
||||
public int? AccountId { get; set; }
|
||||
/// <summary>Optional job costing link.</summary>
|
||||
public int? JobId { get; set; }
|
||||
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public decimal Quantity { get; set; } = 1;
|
||||
public decimal UnitPrice { get; set; }
|
||||
public decimal Amount { get; set; }
|
||||
public int DisplayOrder { get; set; }
|
||||
|
||||
// Navigation
|
||||
public virtual Bill Bill { get; set; } = null!;
|
||||
public virtual Account? Account { get; set; }
|
||||
public virtual Job? Job { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A payment made against a vendor bill.
|
||||
/// </summary>
|
||||
public class BillPayment : BaseEntity
|
||||
{
|
||||
public string PaymentNumber { get; set; } = string.Empty;
|
||||
public int BillId { get; set; }
|
||||
/// <summary>Denormalized for AP reporting without joining through Bill.</summary>
|
||||
public int VendorId { get; set; }
|
||||
/// <summary>Bank/cash account the payment came out of.</summary>
|
||||
public int BankAccountId { get; set; }
|
||||
|
||||
public DateTime PaymentDate { get; set; } = DateTime.UtcNow;
|
||||
public decimal Amount { get; set; }
|
||||
public PaymentMethod PaymentMethod { get; set; }
|
||||
public string? CheckNumber { get; set; }
|
||||
public string? Memo { get; set; }
|
||||
|
||||
// Navigation
|
||||
public virtual Bill Bill { get; set; } = null!;
|
||||
public virtual Vendor Vendor { get; set; } = null!;
|
||||
public virtual Account BankAccount { get; set; } = null!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A direct expense paid immediately (not via a bill). Covers cash/card purchases.
|
||||
/// </summary>
|
||||
public class Expense : BaseEntity
|
||||
{
|
||||
public string ExpenseNumber { get; set; } = string.Empty;
|
||||
public DateTime Date { get; set; } = DateTime.UtcNow;
|
||||
|
||||
/// <summary>Optional vendor the expense was paid to.</summary>
|
||||
public int? VendorId { get; set; }
|
||||
/// <summary>Expense category account (e.g. 6200 Powder & Materials).</summary>
|
||||
public int ExpenseAccountId { get; set; }
|
||||
/// <summary>Account money came out of (e.g. 1000 Checking, 2100 Credit Card).</summary>
|
||||
public int PaymentAccountId { get; set; }
|
||||
/// <summary>Optional job costing link.</summary>
|
||||
public int? JobId { get; set; }
|
||||
|
||||
public PaymentMethod PaymentMethod { get; set; }
|
||||
public decimal Amount { get; set; }
|
||||
public string? Memo { get; set; }
|
||||
public string? ReceiptFilePath { get; set; }
|
||||
|
||||
// Navigation
|
||||
public virtual Vendor? Vendor { get; set; }
|
||||
public virtual Account ExpenseAccount { get; set; } = null!;
|
||||
public virtual Account PaymentAccount { get; set; } = null!;
|
||||
public virtual Job? Job { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Stores the raw AI prediction data captured at analysis time.
|
||||
/// Both QuoteItem and JobItem carry a nullable FK to this table so the
|
||||
/// same prediction record is shared when a quote converts to a job —
|
||||
/// no duplication, clean lineage for reporting.
|
||||
/// </summary>
|
||||
public class AiItemPrediction : BaseEntity
|
||||
{
|
||||
// Raw AI predictions — captured once, never mutated
|
||||
public decimal PredictedSurfaceAreaSqFt { get; set; }
|
||||
public int PredictedMinutes { get; set; }
|
||||
public string PredictedComplexity { get; set; } = "Moderate"; // Simple|Moderate|Complex|Extreme
|
||||
public decimal PredictedUnitPrice { get; set; } // EstimatedUnitPrice shown to user in wizard
|
||||
public string Confidence { get; set; } = "Medium"; // Low|Medium|High
|
||||
public string? Reasoning { get; set; } // AI explanation text
|
||||
public string? AiTags { get; set; } // Comma-separated tags from fixed taxonomy
|
||||
public int ConversationRounds { get; set; } = 1; // How many follow-up rounds were needed
|
||||
|
||||
// Set when the item is saved: was the AI surface area or price overridden by the user?
|
||||
public bool UserOverrodeEstimate { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Immutable audit log of every Anthropic API call made on behalf of a tenant.
|
||||
/// Intentionally omits BaseEntity — no soft delete, no UpdatedAt, no per-tenant query filter
|
||||
/// (this is a platform-wide log read by SuperAdmins across all companies).
|
||||
/// </summary>
|
||||
public class AiUsageLog
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public int CompanyId { get; set; }
|
||||
public string UserId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Which AI feature triggered this call. Use AppConstants.AiFeatures constants.</summary>
|
||||
public string Feature { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>True when the AI service returned successfully; false on exception or upstream error.</summary>
|
||||
public bool Success { get; set; } = true;
|
||||
|
||||
/// <summary>Approximate input size in characters/bytes — used as a rough token-cost proxy.</summary>
|
||||
public int InputLength { get; set; }
|
||||
|
||||
public DateTime CalledAt { get; set; }
|
||||
|
||||
public Company? Company { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Platform-wide announcements shown as dismissible banners to company users.
|
||||
/// Not a BaseEntity — SuperAdmin-managed, not per-tenant.
|
||||
/// </summary>
|
||||
public class Announcement
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Message { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>info | success | warning | danger</summary>
|
||||
public string Type { get; set; } = "info";
|
||||
|
||||
/// <summary>All | Plan | Company</summary>
|
||||
public string Target { get; set; } = "All";
|
||||
|
||||
/// <summary>Populated when Target == "Plan" (matches Company.SubscriptionPlan int)</summary>
|
||||
public int? TargetPlan { get; set; }
|
||||
|
||||
/// <summary>Populated when Target == "Company"</summary>
|
||||
public int? TargetCompanyId { get; set; }
|
||||
|
||||
public DateTime StartsAt { get; set; } = DateTime.UtcNow;
|
||||
public DateTime? ExpiresAt { get; set; }
|
||||
public bool IsDismissible { get; set; } = true;
|
||||
public bool IsActive { get; set; } = true;
|
||||
public string CreatedByUserId { get; set; } = string.Empty;
|
||||
public string CreatedByUserName { get; set; } = string.Empty;
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
|
||||
public virtual ICollection<AnnouncementDismissal> Dismissals { get; set; } = new List<AnnouncementDismissal>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records that a specific user has dismissed a specific announcement.
|
||||
/// </summary>
|
||||
public class AnnouncementDismissal
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int AnnouncementId { get; set; }
|
||||
public string UserId { get; set; } = string.Empty;
|
||||
public DateTime DismissedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public virtual Announcement Announcement { get; set; } = null!;
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using PowderCoating.Core.Enums;
|
||||
using System.Linq;
|
||||
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
public class ApplicationUser : IdentityUser
|
||||
{
|
||||
// Multi-tenancy
|
||||
public int CompanyId { get; set; }
|
||||
public virtual Company? Company { get; set; } // Nullable to avoid EF query filter issues
|
||||
public string? CompanyRole { get; set; } // CompanyAdmin, Manager, Worker, Viewer
|
||||
|
||||
// Basic Information
|
||||
public string FirstName { get; set; } = string.Empty;
|
||||
public string LastName { get; set; } = string.Empty;
|
||||
public string? EmployeeNumber { get; set; }
|
||||
|
||||
public DateTime HireDate { get; set; }
|
||||
public DateTime? TerminationDate { get; set; }
|
||||
|
||||
public string? Address { get; set; }
|
||||
public string? City { get; set; }
|
||||
public string? State { get; set; }
|
||||
public string? ZipCode { get; set; }
|
||||
|
||||
public string? Department { get; set; }
|
||||
public string? Position { get; set; }
|
||||
|
||||
// User Preferences
|
||||
public string? Theme { get; set; } = "light";
|
||||
public string? DateFormat { get; set; } = "MM/dd/yyyy";
|
||||
public string? TimeZone { get; set; } = "America/New_York";
|
||||
public int? DashboardLayout { get; set; }
|
||||
|
||||
// Settings
|
||||
public bool IsActive { get; set; } = true;
|
||||
public bool CanViewShopFloor { get; set; } = true;
|
||||
public bool CanManageJobs { get; set; } = false;
|
||||
public bool CanManageInventory { get; set; } = false;
|
||||
public bool CanManageCustomers { get; set; } = false;
|
||||
public bool CanCreateQuotes { get; set; } = false;
|
||||
public bool CanApproveQuotes { get; set; } = false;
|
||||
public bool CanManageCalendar { get; set; } = false;
|
||||
public bool CanViewCalendar { get; set; } = true;
|
||||
public bool CanManageProducts { get; set; } = false;
|
||||
public bool CanViewProducts { get; set; } = true;
|
||||
public bool CanManageEquipment { get; set; } = false;
|
||||
public bool CanManageVendors { get; set; } = false;
|
||||
public bool CanManageMaintenance { get; set; } = false;
|
||||
public bool CanManageInvoices { get; set; } = false;
|
||||
public bool CanViewReports { get; set; } = false;
|
||||
|
||||
// Profile Photo (filesystem storage)
|
||||
public string? ProfilePictureFilePath { get; set; } // Relative path from ContentRoot/media/ (e.g., "123/profile-photos/user-abc.jpg")
|
||||
|
||||
public string? SidebarColor { get; set; } = "ocean";
|
||||
public string? Notes { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
public DateTime? LastLoginDate { get; set; }
|
||||
|
||||
// Ban
|
||||
public bool IsBanned { get; set; } = false;
|
||||
public DateTime? BannedAt { get; set; }
|
||||
public string? BanReason { get; set; }
|
||||
public string? BannedByUserId { get; set; }
|
||||
|
||||
// Relationships
|
||||
public virtual ICollection<Quote> PreparedQuotes { get; set; } = new List<Quote>();
|
||||
public virtual ICollection<MaintenanceRecord> PerformedMaintenances { get; set; } = new List<MaintenanceRecord>();
|
||||
|
||||
// Full name helper
|
||||
public string FullName => $"{FirstName} {LastName}";
|
||||
|
||||
/// <summary>
|
||||
/// Sets every <c>Can*</c> boolean permission property to <c>true</c>.
|
||||
/// Use this when creating a first-time company admin so that any permission added
|
||||
/// to this class in the future is automatically granted without needing to update
|
||||
/// every account-creation path individually.
|
||||
/// </summary>
|
||||
public void GrantAllPermissions()
|
||||
{
|
||||
foreach (var prop in typeof(ApplicationUser).GetProperties()
|
||||
.Where(p => p.Name.StartsWith("Can") &&
|
||||
p.PropertyType == typeof(bool) &&
|
||||
p.CanWrite))
|
||||
{
|
||||
prop.SetValue(this, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a scheduled appointment for customer drop-offs, pick-ups, consultations, or job work.
|
||||
/// </summary>
|
||||
public class Appointment : BaseEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Auto-generated appointment number in format APT-YYMM-####
|
||||
/// </summary>
|
||||
public string AppointmentNumber { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Brief title/description of the appointment
|
||||
/// </summary>
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Detailed description and notes about the appointment
|
||||
/// </summary>
|
||||
public string? Description { get; set; }
|
||||
|
||||
// Customer Information
|
||||
/// <summary>
|
||||
/// Optional foreign key to Customer (not required for internal appointments like employee days off)
|
||||
/// </summary>
|
||||
public int? CustomerId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional foreign key to Job (required for JOB_WORK appointment type)
|
||||
/// </summary>
|
||||
public int? JobId { get; set; }
|
||||
|
||||
// Lookup Foreign Keys
|
||||
/// <summary>
|
||||
/// Foreign key to AppointmentStatusLookup
|
||||
/// </summary>
|
||||
public int AppointmentStatusId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Foreign key to AppointmentTypeLookup
|
||||
/// </summary>
|
||||
public int AppointmentTypeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional foreign key to ApplicationUser assigned to handle this appointment
|
||||
/// </summary>
|
||||
public string? AssignedUserId { get; set; }
|
||||
|
||||
// Timing
|
||||
/// <summary>
|
||||
/// Scheduled start date and time (UTC)
|
||||
/// </summary>
|
||||
public DateTime ScheduledStartTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Scheduled end date and time (UTC)
|
||||
/// </summary>
|
||||
public DateTime ScheduledEndTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this is an all-day appointment (hides specific times)
|
||||
/// </summary>
|
||||
public bool IsAllDay { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Actual start time when customer arrived (UTC, nullable until appointment starts)
|
||||
/// </summary>
|
||||
public DateTime? ActualStartTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Actual end time when appointment finished (UTC, nullable until appointment completes)
|
||||
/// </summary>
|
||||
public DateTime? ActualEndTime { get; set; }
|
||||
|
||||
// Additional Information
|
||||
/// <summary>
|
||||
/// Optional location at the shop (e.g., "Main Office", "Loading Dock", "Inspection Area")
|
||||
/// </summary>
|
||||
public string? Location { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Internal notes for staff (not visible to customer)
|
||||
/// </summary>
|
||||
public string? Notes { get; set; }
|
||||
|
||||
// Reminder Settings
|
||||
/// <summary>
|
||||
/// Whether to send reminder notifications
|
||||
/// </summary>
|
||||
public bool IsReminderEnabled { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// How many minutes before appointment to send reminder (default 30 minutes)
|
||||
/// </summary>
|
||||
public int ReminderMinutesBefore { get; set; } = 30;
|
||||
|
||||
// Navigation Properties
|
||||
public virtual Customer? Customer { get; set; }
|
||||
public virtual Job? Job { get; set; }
|
||||
public virtual AppointmentStatusLookup AppointmentStatus { get; set; } = null!;
|
||||
public virtual AppointmentTypeLookup AppointmentType { get; set; } = null!;
|
||||
public virtual ApplicationUser? AssignedUser { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Company-specific appointment status lookup table.
|
||||
/// Enables workflow customization for appointment lifecycle management.
|
||||
/// </summary>
|
||||
public class AppointmentStatusLookup : BaseEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Immutable status code used in code logic (e.g., "SCHEDULED", "CONFIRMED", "COMPLETED").
|
||||
/// Acts as the identifier for status-specific business rules.
|
||||
/// </summary>
|
||||
public string StatusCode { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// User-customizable display name shown in UI (e.g., "Scheduled", "Confirmed", "No Show").
|
||||
/// </summary>
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Workflow sequence number for ordering statuses (1 = first, higher = later).
|
||||
/// </summary>
|
||||
public int DisplayOrder { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Bootstrap color class for badge styling (primary, success, danger, warning, info, secondary, dark).
|
||||
/// </summary>
|
||||
public string ColorClass { get; set; } = "secondary";
|
||||
|
||||
/// <summary>
|
||||
/// Optional Bootstrap icon class (e.g., "bi-calendar-check", "bi-clock", "bi-x-circle").
|
||||
/// </summary>
|
||||
public string? IconClass { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this status is currently active and available for selection.
|
||||
/// </summary>
|
||||
public bool IsActive { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// System-defined statuses cannot be deleted (SCHEDULED, COMPLETED, CANCELLED).
|
||||
/// Prevents breaking core appointment workflow.
|
||||
/// </summary>
|
||||
public bool IsSystemDefined { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this status represents a completed appointment (COMPLETED, CANCELLED, NO_SHOW).
|
||||
/// Used for reporting and filtering.
|
||||
/// </summary>
|
||||
public bool IsTerminalStatus { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Optional description explaining when to use this status.
|
||||
/// </summary>
|
||||
public string? Description { get; set; }
|
||||
|
||||
// Navigation properties
|
||||
public virtual ICollection<Appointment> Appointments { get; set; } = new List<Appointment>();
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Company-specific appointment type lookup table.
|
||||
/// Defines different categories of appointments (drop-off, pick-up, consultation, job work).
|
||||
/// </summary>
|
||||
public class AppointmentTypeLookup : BaseEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Immutable type code used in code logic (e.g., "DROP_OFF", "PICK_UP", "CONSULTATION", "JOB_WORK").
|
||||
/// </summary>
|
||||
public string TypeCode { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// User-customizable display name shown in UI (e.g., "Customer Drop-Off", "Customer Pick-Up").
|
||||
/// </summary>
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Sort order for displaying types in dropdowns and filters.
|
||||
/// </summary>
|
||||
public int DisplayOrder { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Bootstrap color class for calendar event styling (purple, green, blue, orange, etc.).
|
||||
/// Each type gets a distinct color for visual differentiation.
|
||||
/// </summary>
|
||||
public string ColorClass { get; set; } = "primary";
|
||||
|
||||
/// <summary>
|
||||
/// Optional Bootstrap icon class for calendar events (e.g., "bi-box-arrow-down", "bi-box-arrow-up").
|
||||
/// </summary>
|
||||
public string? IconClass { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether appointments of this type require a Job to be linked.
|
||||
/// True for JOB_WORK type, false for others.
|
||||
/// </summary>
|
||||
public bool RequiresJobLink { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this type is currently active and available for selection.
|
||||
/// </summary>
|
||||
public bool IsActive { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// System-defined types cannot be deleted (the 4 default types).
|
||||
/// Prevents breaking core appointment functionality.
|
||||
/// </summary>
|
||||
public bool IsSystemDefined { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Optional description explaining when to use this appointment type.
|
||||
/// </summary>
|
||||
public string? Description { get; set; }
|
||||
|
||||
// Navigation properties
|
||||
public virtual ICollection<Appointment> Appointments { get; set; } = new List<Appointment>();
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Platform-wide audit trail. Not a BaseEntity — no soft delete, no tenant filter.
|
||||
/// </summary>
|
||||
public class AuditLog
|
||||
{
|
||||
public long Id { get; set; }
|
||||
|
||||
// Who
|
||||
public string? UserId { get; set; }
|
||||
public string UserName { get; set; } = "System";
|
||||
public int? CompanyId { get; set; }
|
||||
public string? CompanyName { get; set; }
|
||||
|
||||
// What
|
||||
public string Action { get; set; } = string.Empty; // Created | Updated | Deleted | Restored | Login | ManualChange
|
||||
public string EntityType { get; set; } = string.Empty; // Customer | Job | Quote | …
|
||||
public string? EntityId { get; set; }
|
||||
public string? EntityDescription { get; set; } // Human-readable label (e.g. "JOB-2501-0042")
|
||||
|
||||
// Change detail (JSON)
|
||||
public string? OldValues { get; set; }
|
||||
public string? NewValues { get; set; }
|
||||
|
||||
// Context
|
||||
public string? IpAddress { get; set; }
|
||||
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// A platform-level IP address block. When an IP is active here, login attempts
|
||||
/// from that address are rejected before Identity even checks credentials.
|
||||
/// No BaseEntity — no company scope, no soft delete; hard-delete is fine for bans.
|
||||
/// </summary>
|
||||
public class BannedIp
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string IpAddress { get; set; } = string.Empty;
|
||||
public string? Reason { get; set; }
|
||||
public string? BannedByUserId { get; set; }
|
||||
public DateTime BannedAt { get; set; } = DateTime.UtcNow;
|
||||
public DateTime? ExpiresAt { get; set; } // null = permanent
|
||||
public bool IsActive { get; set; } = true;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Base entity class that all domain entities inherit from
|
||||
/// </summary>
|
||||
public abstract class BaseEntity
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
// Multi-tenancy
|
||||
public int CompanyId { get; set; }
|
||||
|
||||
// Audit fields
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
public string? CreatedBy { get; set; }
|
||||
public string? UpdatedBy { get; set; }
|
||||
|
||||
// Soft delete
|
||||
public bool IsDeleted { get; set; } = false;
|
||||
public DateTime? DeletedAt { get; set; }
|
||||
public string? DeletedBy { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using PowderCoating.Core.Enums;
|
||||
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
public class BugReport : BaseEntity
|
||||
{
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public string SubmittedByUserId { get; set; } = string.Empty;
|
||||
public string SubmittedByUserName { get; set; } = string.Empty;
|
||||
public string? CompanyName { get; set; }
|
||||
public BugReportPriority Priority { get; set; } = BugReportPriority.Normal;
|
||||
public BugReportStatus Status { get; set; } = BugReportStatus.New;
|
||||
public string? ResolutionNotes { get; set; }
|
||||
public DateTime? ResolvedAt { get; set; }
|
||||
public string? ResolvedBy { get; set; }
|
||||
|
||||
public ICollection<BugReportAttachment> Attachments { get; set; } = new List<BugReportAttachment>();
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
public class BugReportAttachment : BaseEntity
|
||||
{
|
||||
public int BugReportId { get; set; }
|
||||
public BugReport BugReport { get; set; } = null!;
|
||||
|
||||
public string BlobPath { get; set; } = string.Empty;
|
||||
public string FileName { get; set; } = string.Empty;
|
||||
public string ContentType { get; set; } = string.Empty;
|
||||
public long FileSizeBytes { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace PowderCoating.Core.Entities
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a hierarchical category in the product catalog.
|
||||
/// Supports multi-level nesting for organizing catalog items.
|
||||
/// </summary>
|
||||
public class CatalogCategory : BaseEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the category name.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the optional category description.
|
||||
/// </summary>
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the parent category ID for hierarchical organization.
|
||||
/// Null indicates this is a root-level category.
|
||||
/// </summary>
|
||||
public int? ParentCategoryId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the parent category navigation property.
|
||||
/// </summary>
|
||||
public virtual CatalogCategory? ParentCategory { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the collection of child categories.
|
||||
/// </summary>
|
||||
public virtual ICollection<CatalogCategory> SubCategories { get; set; } = new List<CatalogCategory>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the display order for sorting categories at the same level.
|
||||
/// Lower numbers appear first.
|
||||
/// </summary>
|
||||
public int DisplayOrder { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether this category is currently active and visible.
|
||||
/// </summary>
|
||||
public bool IsActive { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// When true, items in this category are retail merchandise available for direct sale
|
||||
/// (e.g. branded apparel, cleaning products). New items inherit this flag.
|
||||
/// </summary>
|
||||
public bool IsMerchandise { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the collection of catalog items in this category.
|
||||
/// </summary>
|
||||
public virtual ICollection<CatalogItem> Items { get; set; } = new List<CatalogItem>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
namespace PowderCoating.Core.Entities
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a reusable item in the product catalog that can be quickly added to quotes.
|
||||
/// Stores default values for common powder coating jobs.
|
||||
/// </summary>
|
||||
public class CatalogItem : BaseEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the item name.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the optional item description.
|
||||
/// </summary>
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the optional SKU (Stock Keeping Unit) or item code.
|
||||
/// </summary>
|
||||
public string? SKU { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the category ID this item belongs to.
|
||||
/// </summary>
|
||||
public int CategoryId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the category navigation property.
|
||||
/// </summary>
|
||||
public virtual CatalogCategory Category { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the default price for this item.
|
||||
/// Can be overridden when added to a quote.
|
||||
/// </summary>
|
||||
public decimal DefaultPrice { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether this item typically requires sandblasting.
|
||||
/// </summary>
|
||||
public bool DefaultRequiresSandblasting { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether this item typically requires masking/taping.
|
||||
/// </summary>
|
||||
public bool DefaultRequiresMasking { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the estimated processing time in minutes.
|
||||
/// </summary>
|
||||
public int? DefaultEstimatedMinutes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the approximate surface area in square feet (or square meters if using metric).
|
||||
/// </summary>
|
||||
public decimal? ApproximateArea { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the display order for sorting items within a category.
|
||||
/// Lower numbers appear first.
|
||||
/// </summary>
|
||||
public int DisplayOrder { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether this item is currently active and visible.
|
||||
/// </summary>
|
||||
public bool IsActive { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// When true, this item is retail merchandise available for direct sale on an invoice
|
||||
/// without a job (e.g. branded apparel, cleaning products). Inherited from the category
|
||||
/// by default but can be overridden per item.
|
||||
/// </summary>
|
||||
public bool IsMerchandise { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Optional link to an inventory item so stock is decremented when this item is sold.
|
||||
/// </summary>
|
||||
public int? InventoryItemId { get; set; }
|
||||
public virtual InventoryItem? InventoryItem { get; set; }
|
||||
|
||||
// ── Financial Account Mapping ──────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the revenue account ID for invoicing this item.
|
||||
/// When null, falls back to the default revenue account.
|
||||
/// </summary>
|
||||
public int? RevenueAccountId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the COGS account ID for recording material cost when this item is used.
|
||||
/// When null, falls back to the default COGS account.
|
||||
/// </summary>
|
||||
public int? CogsAccountId { get; set; }
|
||||
|
||||
public virtual Account? RevenueAccount { get; set; }
|
||||
public virtual Account? CogsAccount { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
using PowderCoating.Core.Enums;
|
||||
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a company/tenant in the multi-tenant system
|
||||
/// </summary>
|
||||
public class Company : BaseEntity
|
||||
{
|
||||
// Basic Information
|
||||
public string CompanyName { get; set; } = string.Empty;
|
||||
public string? CompanyCode { get; set; } // Short code (e.g., ABC)
|
||||
|
||||
// Contact Information
|
||||
public string PrimaryContactName { get; set; } = string.Empty;
|
||||
public string PrimaryContactEmail { get; set; } = string.Empty;
|
||||
public string? Phone { get; set; }
|
||||
|
||||
// Address
|
||||
public string? Address { get; set; }
|
||||
public string? City { get; set; }
|
||||
public string? State { get; set; }
|
||||
public string? ZipCode { get; set; }
|
||||
|
||||
// Subscription/Status
|
||||
public bool IsActive { get; set; } = true;
|
||||
public DateTime SubscriptionStartDate { get; set; } = DateTime.UtcNow;
|
||||
public DateTime? SubscriptionEndDate { get; set; }
|
||||
public int SubscriptionPlan { get; set; } = 0;
|
||||
public SubscriptionStatus SubscriptionStatus { get; set; } = SubscriptionStatus.Active;
|
||||
public string? StripeCustomerId { get; set; }
|
||||
public string? StripeSubscriptionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When true the company has complimentary/internal access.
|
||||
/// All plan limits are treated as unlimited and the subscription
|
||||
/// expiry banner/lockout is suppressed regardless of SubscriptionEndDate.
|
||||
/// </summary>
|
||||
public bool IsComped { get; set; } = false;
|
||||
|
||||
// Stripe Connect — online invoice payments
|
||||
public string? StripeAccountId { get; set; } // acct_xxx from OAuth
|
||||
public StripeConnectStatus StripeConnectStatus { get; set; } = StripeConnectStatus.NotConnected;
|
||||
public OnlinePaymentSurchargeType OnlinePaymentSurchargeType { get; set; } = OnlinePaymentSurchargeType.None;
|
||||
public decimal OnlinePaymentSurchargeValue { get; set; } = 0; // % or flat $ depending on type
|
||||
public bool OnlineSurchargeAcknowledged { get; set; } = false; // shop accepted compliance disclaimer
|
||||
|
||||
/// <summary>Internal notes about manual subscription changes (not shown to the company).</summary>
|
||||
public string? SubscriptionNotes { get; set; }
|
||||
|
||||
// Per-company limit overrides. null = use plan config default. -1 = unlimited.
|
||||
public int? MaxUsersOverride { get; set; }
|
||||
public int? MaxActiveJobsOverride { get; set; }
|
||||
public int? MaxCustomersOverride { get; set; }
|
||||
public int? MaxQuotesOverride { get; set; }
|
||||
public int? MaxCatalogItemsOverride { get; set; }
|
||||
public int? MaxJobPhotosOverride { get; set; }
|
||||
public int? MaxQuotePhotosOverride { get; set; }
|
||||
/// <summary>null = use plan config default. -1 = unlimited. 0 = disabled for this company.</summary>
|
||||
public int? MaxAiPhotoQuotesPerMonthOverride { get; set; }
|
||||
|
||||
// AI Feature Flags (SuperAdmin-controlled per company)
|
||||
/// <summary>Enables/disables AI Photo Quote analysis for this company.</summary>
|
||||
public bool AiPhotoQuotesEnabled { get; set; } = true;
|
||||
/// <summary>Enables/disables the AI Inventory Assist lookup for this company.</summary>
|
||||
public bool AiInventoryAssistEnabled { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Stores the billing period the customer selected at registration (or last changed on the Billing page).
|
||||
/// true = annual billing, false = monthly billing.
|
||||
/// Used when redirecting to Stripe Checkout at trial-end so the right price ID is used automatically.
|
||||
/// </summary>
|
||||
public bool IsAnnualBilling { get; set; } = false;
|
||||
|
||||
// Per-company feature overrides (SuperAdmin-controlled)
|
||||
/// <summary>
|
||||
/// null = use plan config default.
|
||||
/// true = force-enable online payments for this company regardless of plan.
|
||||
/// false = force-disable online payments for this company regardless of plan.
|
||||
/// </summary>
|
||||
public bool? OnlinePaymentsOverride { get; set; }
|
||||
/// <summary>
|
||||
/// null = use plan config default.
|
||||
/// true = force-enable accounting module for this company regardless of plan.
|
||||
/// false = force-disable accounting module for this company regardless of plan.
|
||||
/// </summary>
|
||||
public bool? AccountingOverride { get; set; }
|
||||
|
||||
// Email marketing opt-out (CAN-SPAM compliance for platform broadcast emails)
|
||||
public bool MarketingEmailOptOut { get; set; } = false;
|
||||
public string MarketingUnsubscribeToken { get; set; } = Guid.NewGuid().ToString("N");
|
||||
|
||||
// Settings
|
||||
public string? TimeZone { get; set; } = "America/New_York";
|
||||
public byte[]? LogoData { get; set; } // Legacy - kept for backward compatibility
|
||||
public string? LogoContentType { get; set; } // Legacy - kept for backward compatibility
|
||||
public string? LogoFilePath { get; set; } // Filesystem path: /media/{CompanyId}/company-logo.{ext}
|
||||
// Navigation Properties
|
||||
public virtual ICollection<ApplicationUser> Users { get; set; } = new List<ApplicationUser>();
|
||||
public virtual ICollection<Customer> Customers { get; set; } = new List<Customer>();
|
||||
public virtual ICollection<Job> Jobs { get; set; } = new List<Job>();
|
||||
public virtual ICollection<Equipment> Equipment { get; set; } = new List<Equipment>();
|
||||
public virtual ICollection<Quote> Quotes { get; set; } = new List<Quote>();
|
||||
public virtual ICollection<InventoryItem> InventoryItems { get; set; } = new List<InventoryItem>();
|
||||
public virtual ICollection<Vendor> Vendors { get; set; } = new List<Vendor>();
|
||||
public virtual ICollection<ShopWorker> ShopWorkers { get; set; } = new List<ShopWorker>();
|
||||
public virtual ICollection<PricingTier> PricingTiers { get; set; } = new List<PricingTier>();
|
||||
public virtual CompanyOperatingCosts? OperatingCosts { get; set; }
|
||||
public virtual CompanyPreferences? Preferences { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using PowderCoating.Core.Enums;
|
||||
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// A named blast setup for a company (e.g. "Siphon Cabinet", "Pressure Pot #1", "Blast Room").
|
||||
/// Companies can have multiple setups; one is flagged as the default used for AI quoting when
|
||||
/// the user doesn't pick a specific one.
|
||||
/// </summary>
|
||||
public class CompanyBlastSetup : BaseEntity
|
||||
{
|
||||
/// <summary>Friendly name shown in dropdowns, e.g. "Main Cabinet" or "Outdoor Blast Pot".</summary>
|
||||
[Required]
|
||||
[StringLength(100)]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
public BlastSetupType SetupType { get; set; } = BlastSetupType.PressurePot;
|
||||
|
||||
/// <summary>Compressor CFM available at the blast nozzle.</summary>
|
||||
[Range(0, 9999)]
|
||||
public decimal CompressorCfm { get; set; }
|
||||
|
||||
/// <summary>Nozzle orifice number (2–8).</summary>
|
||||
[Range(2, 8)]
|
||||
public int BlastNozzleSize { get; set; } = 5;
|
||||
|
||||
public BlastSubstrateType PrimarySubstrate { get; set; } = BlastSubstrateType.Mixed;
|
||||
|
||||
/// <summary>
|
||||
/// When set, bypasses the derived formula and uses this exact rate (sqft/hr).
|
||||
/// Useful for shops that have measured their own throughput.
|
||||
/// </summary>
|
||||
[Range(0, 99999)]
|
||||
public decimal? BlastRateSqFtPerHourOverride { get; set; }
|
||||
|
||||
/// <summary>Used as the default rate when no setup is explicitly selected in the wizard.</summary>
|
||||
public bool IsDefault { get; set; }
|
||||
|
||||
public bool IsActive { get; set; } = true;
|
||||
|
||||
public int DisplayOrder { get; set; }
|
||||
|
||||
// Navigation
|
||||
public virtual Company Company { get; set; } = null!;
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using PowderCoating.Core.Enums;
|
||||
|
||||
namespace PowderCoating.Core.Entities
|
||||
{
|
||||
public class CompanyOperatingCosts : BaseEntity
|
||||
{
|
||||
// Navigation
|
||||
public new int CompanyId { get; set; }
|
||||
public virtual Company Company { get; set; } = null!;
|
||||
|
||||
// Labor Rates (per hour, in currency)
|
||||
[Range(0, 10000)]
|
||||
public decimal StandardLaborRate { get; set; }
|
||||
|
||||
// Additional Coat Labor Percentage (percentage of base labor for each additional coat beyond the first)
|
||||
[Range(0, 100)]
|
||||
public decimal AdditionalCoatLaborPercent { get; set; } = 30m;
|
||||
|
||||
// Equipment Operating Costs (per hour)
|
||||
[Range(0, 10000)]
|
||||
public decimal OvenOperatingCostPerHour { get; set; }
|
||||
|
||||
[Range(0, 10000)]
|
||||
public decimal SandblasterCostPerHour { get; set; }
|
||||
|
||||
[Range(0, 10000)]
|
||||
public decimal CoatingBoothCostPerHour { get; set; }
|
||||
|
||||
// Material Costs
|
||||
[Range(0, 1000)]
|
||||
public decimal PowderCoatingCostPerSqFt { get; set; } // per square foot
|
||||
|
||||
// Markup / Margin
|
||||
/// <summary>Whether markup is applied to material only, or margin is applied to total item cost.</summary>
|
||||
public PricingMode PricingMode { get; set; } = PricingMode.MarkupOnMaterial;
|
||||
|
||||
/// <summary>Markup % added on top of material costs (used when PricingMode = MarkupOnMaterial).</summary>
|
||||
[Range(0, 100)]
|
||||
public decimal GeneralMarkupPercentage { get; set; }
|
||||
|
||||
/// <summary>Target gross margin % applied to total item cost (used when PricingMode = MarginOnTotalCost).</summary>
|
||||
[Range(0, 99)]
|
||||
public decimal TargetMarginPercent { get; set; }
|
||||
|
||||
// Tax Percentage
|
||||
[Range(0, 100)]
|
||||
public decimal TaxPercent { get; set; }
|
||||
|
||||
// Shop Supplies Rate (percentage applied to materials/labor)
|
||||
[Range(0, 100)]
|
||||
public decimal ShopSuppliesRate { get; set; }
|
||||
|
||||
// Oven batch defaults
|
||||
[Range(1, 1440)]
|
||||
public int DefaultOvenCycleMinutes { get; set; } = 45;
|
||||
|
||||
// Rush Charge
|
||||
public string RushChargeType { get; set; } = "Percentage"; // "Percentage" or "FixedAmount"
|
||||
|
||||
[Range(0, 100)]
|
||||
public decimal RushChargePercentage { get; set; }
|
||||
|
||||
[Range(0, 100000)]
|
||||
public decimal RushChargeFixedAmount { get; set; }
|
||||
|
||||
// Shop Minimum
|
||||
[Range(0, 100000)]
|
||||
public decimal ShopMinimumCharge { get; set; }
|
||||
|
||||
// Part Complexity Multipliers (% added to calculated item price)
|
||||
[Range(0, 500)]
|
||||
public decimal ComplexitySimplePercent { get; set; } = 0m;
|
||||
|
||||
[Range(0, 500)]
|
||||
public decimal ComplexityModeratePercent { get; set; } = 5m;
|
||||
|
||||
[Range(0, 500)]
|
||||
public decimal ComplexityComplexPercent { get; set; } = 15m;
|
||||
|
||||
[Range(0, 500)]
|
||||
public decimal ComplexityExtremePercent { get; set; } = 25m;
|
||||
|
||||
/// <summary>
|
||||
/// Free-text description of this shop's specialties, typical item types, and pricing style.
|
||||
/// Injected into the AI system prompt so the model can calibrate estimates for this company.
|
||||
/// Example: "We specialize in automotive restoration parts — wheels, frames, brackets.
|
||||
/// We charge premium rates and rarely work on items over 20 sqft."
|
||||
/// </summary>
|
||||
[StringLength(2000)]
|
||||
public string? AiContextProfile { get; set; }
|
||||
|
||||
// ── Shop Capability / Quoting Calibration ─────────────────────────────────────
|
||||
// These fields drive the derived BlastRateSqFtPerHour used in AI photo quoting
|
||||
// and calculated item time suggestions. The Override field lets a shop bypass the
|
||||
// formula and enter their real-world number directly.
|
||||
|
||||
/// <summary>Broad capability tier chosen in Setup Wizard; sets sensible defaults for all fields below.</summary>
|
||||
public ShopCapabilityTier ShopCapabilityTier { get; set; } = ShopCapabilityTier.Small;
|
||||
|
||||
/// <summary>Type of blasting setup — biggest single factor in blast throughput.</summary>
|
||||
public BlastSetupType BlastSetupType { get; set; } = BlastSetupType.SiphonCabinet;
|
||||
|
||||
/// <summary>Compressor CFM available for the blast setup.</summary>
|
||||
[Range(0, 2000)]
|
||||
public decimal CompressorCfm { get; set; } = 0m;
|
||||
|
||||
/// <summary>Blast nozzle size (#3 – #8). Larger nozzle = more media flow.</summary>
|
||||
[Range(3, 8)]
|
||||
public int BlastNozzleSize { get; set; } = 4;
|
||||
|
||||
/// <summary>Primary substrate being removed; affects passes required per sqft.</summary>
|
||||
public BlastSubstrateType PrimaryBlastSubstrate { get; set; } = BlastSubstrateType.Mixed;
|
||||
|
||||
/// <summary>
|
||||
/// Manual blast rate override (sqft/hr). When set, the formula is bypassed entirely.
|
||||
/// Useful for dialed-in shops who know their exact throughput.
|
||||
/// </summary>
|
||||
[Range(0, 5000)]
|
||||
public decimal? BlastRateSqFtPerHourOverride { get; set; }
|
||||
|
||||
/// <summary>Coating gun technology — affects application speed on complex parts.</summary>
|
||||
public CoatingGunType CoatingGunType { get; set; } = CoatingGunType.Corona;
|
||||
|
||||
/// <summary>
|
||||
/// Manual coating application rate override (sqft/hr). When set, bypasses the formula.
|
||||
/// </summary>
|
||||
[Range(0, 5000)]
|
||||
public decimal? CoatingRateSqFtPerHourOverride { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Company-specific application and workflow preferences
|
||||
/// </summary>
|
||||
public class CompanyPreferences : BaseEntity
|
||||
{
|
||||
public int CompanyId { get; set; }
|
||||
|
||||
// Application Defaults
|
||||
public string DefaultCurrency { get; set; } = "USD";
|
||||
public string DefaultDateFormat { get; set; } = "MM/dd/yyyy";
|
||||
public string DefaultTimeFormat { get; set; } = "12h";
|
||||
public string DefaultPaymentTerms { get; set; } = "Net 30";
|
||||
public int DefaultQuoteValidityDays { get; set; } = 30;
|
||||
public string QuoteNumberPrefix { get; set; } = "QT";
|
||||
public string JobNumberPrefix { get; set; } = "JOB";
|
||||
public string InvoiceNumberPrefix { get; set; } = "INV";
|
||||
public bool UseMetricSystem { get; set; } = false; // False = Imperial (ft, lb), True = Metric (m, kg)
|
||||
|
||||
// Job / Workflow Defaults
|
||||
public string DefaultJobPriority { get; set; } = "Normal";
|
||||
public bool RequireCustomerPO { get; set; } = false;
|
||||
public bool AllowCustomerApproval { get; set; } = true;
|
||||
public int DefaultTurnaroundDays { get; set; } = 7;
|
||||
// Email Sender Identity
|
||||
/// <summary>From address used in outgoing emails. Falls back to SendGrid:FromEmail in appsettings when null.</summary>
|
||||
public string? EmailFromAddress { get; set; }
|
||||
/// <summary>From display name used in outgoing emails. Falls back to SendGrid:FromName in appsettings when null.</summary>
|
||||
public string? EmailFromName { get; set; }
|
||||
|
||||
// Notifications & Alerts
|
||||
public bool EmailNotificationsEnabled { get; set; } = true;
|
||||
public bool NotifyOnNewJob { get; set; } = true;
|
||||
public bool NotifyOnNewQuote { get; set; } = true;
|
||||
public bool NotifyOnJobStatusChange { get; set; } = true;
|
||||
public bool NotifyOnQuoteApproval { get; set; } = true;
|
||||
public bool NotifyOnPaymentReceived { get; set; } = true;
|
||||
public int QuoteExpiryWarningDays { get; set; } = 3;
|
||||
public int DueDateWarningDays { get; set; } = 2;
|
||||
public int MaintenanceAlertDays { get; set; } = 7;
|
||||
|
||||
// Payment Reminders
|
||||
/// <summary>When true, the background service will send overdue payment reminder emails.</summary>
|
||||
public bool PaymentRemindersEnabled { get; set; } = false;
|
||||
/// <summary>Comma-separated days-past-due thresholds at which reminders are sent (e.g. "7,14,30").</summary>
|
||||
public string PaymentReminderDays { get; set; } = "7,14,30";
|
||||
|
||||
// Data Retention
|
||||
public int QuoteRetentionYears { get; set; } = 7;
|
||||
public int JobRetentionYears { get; set; } = 7;
|
||||
public int LogRetentionDays { get; set; } = 90;
|
||||
public int AutoArchiveJobsDays { get; set; } = 365;
|
||||
public int DeletedRecordRetentionDays { get; set; } = 30;
|
||||
|
||||
// Quote PDF Template
|
||||
public string QtAccentColor { get; set; } = "#374151";
|
||||
public string? QtDefaultTerms { get; set; }
|
||||
public string? QtFooterNote { get; set; }
|
||||
|
||||
// Invoice PDF Template
|
||||
public string InAccentColor { get; set; } = "#374151";
|
||||
public string? InDefaultTerms { get; set; }
|
||||
public string? InFooterNote { get; set; }
|
||||
|
||||
// Blank Work Order PDF Template
|
||||
public string WoAccentColor { get; set; } = "#374151";
|
||||
public string? WoTerms { get; set; }
|
||||
|
||||
// Setup Wizard Progress
|
||||
public bool SetupWizardStarted { get; set; } = false;
|
||||
public bool SetupWizardCompleted { get; set; } = false;
|
||||
/// <summary>Comma-separated step numbers that have been completed (e.g. "1,2,3")</summary>
|
||||
public string? SetupWizardDoneSteps { get; set; }
|
||||
/// <summary>Comma-separated step numbers the user chose to skip</summary>
|
||||
public string? SetupWizardSkippedSteps { get; set; }
|
||||
/// <summary>UTC timestamp of when the setup wizard was completed. Null if not yet completed.</summary>
|
||||
public DateTime? SetupWizardCompletedAt { get; set; }
|
||||
/// <summary>ASP.NET Identity user ID of the user who completed the setup wizard.</summary>
|
||||
public string? SetupWizardCompletedByUserId { get; set; }
|
||||
/// <summary>Display name of the user who completed the setup wizard, stored at completion time
|
||||
/// to avoid a runtime JOIN to the Identity tables when listing companies.</summary>
|
||||
public string? SetupWizardCompletedByName { get; set; }
|
||||
/// <summary>True when the company indicated they are migrating data from QuickBooks Desktop.</summary>
|
||||
public bool MigratingFromQuickBooks { get; set; } = false;
|
||||
/// <summary>JSON blob persisting QB Migration Wizard step state across sessions.</summary>
|
||||
public string? QbMigrationStateJson { get; set; }
|
||||
|
||||
// Navigation
|
||||
public virtual Company Company { get; set; } = null!;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Persisted record of a contact form submission. Inherits CompanyId from BaseEntity so
|
||||
/// company users can only see their own submissions; SuperAdmin sees all via IsPlatformAdmin filter.
|
||||
/// </summary>
|
||||
public class ContactSubmission : BaseEntity
|
||||
{
|
||||
public string SenderName { get; set; } = string.Empty;
|
||||
public string SenderEmail { get; set; } = string.Empty;
|
||||
public string CompanyName { get; set; } = string.Empty;
|
||||
public string Category { get; set; } = string.Empty;
|
||||
public string Subject { get; set; } = string.Empty;
|
||||
public string Message { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>True once a SuperAdmin has opened/acknowledged the submission.</summary>
|
||||
public bool IsRead { get; set; } = false;
|
||||
public DateTime? ReadAt { get; set; }
|
||||
public string? ReadByUserId { get; set; }
|
||||
public string? ReadByUserName { get; set; }
|
||||
|
||||
/// <summary>Optional internal note added by SuperAdmin (e.g. "Replied via email 4/19").</summary>
|
||||
public string? AdminNotes { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using PowderCoating.Core.Enums;
|
||||
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// A credit issued to a customer — either from a warranty resolution, billing correction,
|
||||
/// or goodwill. Can be applied against a future invoice to reduce the balance due.
|
||||
/// </summary>
|
||||
public class CreditMemo : BaseEntity
|
||||
{
|
||||
public string MemoNumber { get; set; } = string.Empty; // CM-YYMM-####
|
||||
public int CustomerId { get; set; }
|
||||
public int? OriginalInvoiceId { get; set; } // Invoice that prompted the credit
|
||||
public int? ReworkRecordId { get; set; } // If from warranty/rework resolution
|
||||
|
||||
public decimal Amount { get; set; }
|
||||
public decimal AmountApplied { get; set; } // How much has been used so far
|
||||
public decimal RemainingBalance => Amount - AmountApplied;
|
||||
|
||||
public DateTime IssueDate { get; set; } = DateTime.UtcNow;
|
||||
public DateTime? ExpiryDate { get; set; } // Optional expiry
|
||||
public string Reason { get; set; } = string.Empty;
|
||||
public string? Notes { get; set; }
|
||||
|
||||
public CreditMemoStatus Status { get; set; } = CreditMemoStatus.Active;
|
||||
public string? IssuedById { get; set; }
|
||||
|
||||
// Navigation
|
||||
public virtual Customer Customer { get; set; } = null!;
|
||||
public virtual Invoice? OriginalInvoice { get; set; }
|
||||
public virtual ReworkRecord? ReworkRecord { get; set; }
|
||||
public virtual ApplicationUser? IssuedBy { get; set; }
|
||||
public virtual ICollection<CreditMemoApplication> Applications { get; set; } = new List<CreditMemoApplication>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records each time a credit memo is (partially) applied to an invoice.
|
||||
/// </summary>
|
||||
public class CreditMemoApplication : BaseEntity
|
||||
{
|
||||
public int CreditMemoId { get; set; }
|
||||
public int InvoiceId { get; set; }
|
||||
public decimal AmountApplied { get; set; }
|
||||
public DateTime AppliedDate { get; set; } = DateTime.UtcNow;
|
||||
public string? AppliedById { get; set; }
|
||||
|
||||
// Navigation
|
||||
public virtual CreditMemo CreditMemo { get; set; } = null!;
|
||||
public virtual Invoice Invoice { get; set; } = null!;
|
||||
public virtual ApplicationUser? AppliedBy { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
public class Customer : BaseEntity
|
||||
{
|
||||
public string? CompanyName { get; set; }
|
||||
public string? ContactFirstName { get; set; }
|
||||
public string? ContactLastName { get; set; }
|
||||
public string? Email { get; set; }
|
||||
public string? Phone { get; set; }
|
||||
public string? MobilePhone { get; set; }
|
||||
public string? Address { get; set; }
|
||||
public string? City { get; set; }
|
||||
public string? State { get; set; }
|
||||
public string? ZipCode { get; set; }
|
||||
public string? Country { get; set; } = "USA";
|
||||
|
||||
// Business Information
|
||||
public bool IsCommercial { get; set; }
|
||||
public string? TaxId { get; set; }
|
||||
public decimal CreditLimit { get; set; }
|
||||
public decimal CurrentBalance { get; set; }
|
||||
public decimal CreditBalance { get; set; } // Available store credit (credit memos)
|
||||
public string? PaymentTerms { get; set; }
|
||||
public int? PricingTierId { get; set; }
|
||||
|
||||
// Tax Exemption
|
||||
public bool IsTaxExempt { get; set; }
|
||||
public byte[]? TaxExemptCertificateData { get; set; }
|
||||
public string? TaxExemptCertificateContentType { get; set; }
|
||||
public string? TaxExemptCertificateFileName { get; set; }
|
||||
|
||||
// Relationships
|
||||
public virtual PricingTier? PricingTier { get; set; }
|
||||
public virtual ICollection<Job> Jobs { get; set; } = new List<Job>();
|
||||
public virtual ICollection<Quote> Quotes { get; set; } = new List<Quote>();
|
||||
public virtual ICollection<CustomerNote> CustomerNotes { get; set; } = new List<CustomerNote>();
|
||||
|
||||
// Additional fields
|
||||
public string? GeneralNotes { get; set; }
|
||||
public bool IsActive { get; set; } = true;
|
||||
public DateTime? LastContactDate { get; set; }
|
||||
|
||||
// Notification preferences
|
||||
public bool NotifyByEmail { get; set; } = true;
|
||||
// NotifyBySms is only set to true after explicit staff-recorded consent (TCPA compliance)
|
||||
public bool NotifyBySms { get; set; } = false;
|
||||
// Unique token used in email unsubscribe links (no auth required)
|
||||
public string UnsubscribeToken { get; set; } = Guid.NewGuid().ToString("N");
|
||||
// SMS consent tracking (TCPA compliance)
|
||||
public DateTime? SmsConsentedAt { get; set; }
|
||||
public string? SmsConsentMethod { get; set; }
|
||||
/// <summary>Set when the customer replies STOP or is manually opted out. Null means they have never opted out.</summary>
|
||||
public DateTime? SmsOptedOutAt { get; set; }
|
||||
|
||||
public virtual ICollection<NotificationLog> NotificationLogs { get; set; } = new List<NotificationLog>();
|
||||
public virtual ICollection<Invoice> Invoices { get; set; } = new List<Invoice>();
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Platform-wide tip of the day shown on the dashboard welcome section.
|
||||
/// Not tenant-scoped — visible to all companies.
|
||||
/// </summary>
|
||||
public class DashboardTip
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string TipText { get; set; } = string.Empty;
|
||||
public bool IsActive { get; set; } = true;
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using PowderCoating.Core.Enums;
|
||||
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
public class Deposit : BaseEntity
|
||||
{
|
||||
public string ReceiptNumber { get; set; } = string.Empty;
|
||||
public int CustomerId { get; set; }
|
||||
public int? JobId { get; set; }
|
||||
public int? QuoteId { get; set; }
|
||||
public decimal Amount { get; set; }
|
||||
public PaymentMethod PaymentMethod { get; set; }
|
||||
public DateTime ReceivedDate { get; set; } = DateTime.UtcNow;
|
||||
public string? Reference { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
public string? RecordedById { get; set; }
|
||||
|
||||
// Applied to invoice when invoice is created
|
||||
public int? AppliedToInvoiceId { get; set; }
|
||||
public DateTime? AppliedDate { get; set; }
|
||||
|
||||
// Navigation
|
||||
public virtual Customer Customer { get; set; } = null!;
|
||||
public virtual Job? Job { get; set; }
|
||||
public virtual Quote? Quote { get; set; }
|
||||
public virtual Invoice? AppliedToInvoice { get; set; }
|
||||
public virtual ApplicationUser? RecordedBy { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
using PowderCoating.Core.Enums;
|
||||
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
public class Equipment : BaseEntity
|
||||
{
|
||||
public string EquipmentName { get; set; } = string.Empty;
|
||||
public string? EquipmentNumber { get; set; }
|
||||
public string EquipmentType { get; set; } = string.Empty; // Oven, Spray Booth, Compressor, etc.
|
||||
public string? Manufacturer { get; set; }
|
||||
public string? Model { get; set; }
|
||||
public string? SerialNumber { get; set; }
|
||||
|
||||
public DateTime? PurchaseDate { get; set; }
|
||||
public decimal PurchasePrice { get; set; }
|
||||
public DateTime? WarrantyExpiration { get; set; }
|
||||
|
||||
public EquipmentStatus Status { get; set; } = EquipmentStatus.Operational;
|
||||
public string? Location { get; set; }
|
||||
|
||||
// Maintenance Information
|
||||
public int RecommendedMaintenanceIntervalDays { get; set; }
|
||||
public DateTime? LastMaintenanceDate { get; set; }
|
||||
public DateTime? NextScheduledMaintenance { get; set; }
|
||||
|
||||
public string? Notes { get; set; }
|
||||
public bool IsActive { get; set; } = true;
|
||||
|
||||
// Oven-specific capacity (relevant when EquipmentType == "Oven")
|
||||
public decimal? MaxLoadSqFt { get; set; }
|
||||
public int? OvenCycleMinutes { get; set; }
|
||||
|
||||
// User Manual
|
||||
public string? ManualFilePath { get; set; } // Filesystem path: /media/{CompanyId}/equipment-manuals/{equipmentId}/{filename}.pdf
|
||||
public string? ManualFileName { get; set; } // Original filename
|
||||
public long? ManualFileSize { get; set; } // File size in bytes
|
||||
public string? ManualContentType { get; set; } // e.g., "application/pdf"
|
||||
public DateTime? ManualUploadedDate { get; set; }
|
||||
|
||||
// Relationships
|
||||
public virtual ICollection<MaintenanceRecord> MaintenanceRecords { get; set; } = new List<MaintenanceRecord>();
|
||||
public virtual ICollection<OvenBatch> OvenBatches { get; set; } = new List<OvenBatch>();
|
||||
}
|
||||
|
||||
public class MaintenanceRecord : BaseEntity
|
||||
{
|
||||
public int EquipmentId { get; set; }
|
||||
public string MaintenanceType { get; set; } = string.Empty; // Preventive, Repair, Inspection, etc.
|
||||
public MaintenanceStatus Status { get; set; } = MaintenanceStatus.Scheduled;
|
||||
public MaintenancePriority Priority { get; set; } = MaintenancePriority.Normal;
|
||||
|
||||
public DateTime ScheduledDate { get; set; }
|
||||
public DateTime? CompletedDate { get; set; }
|
||||
public string? PerformedById { get; set; } // Changed from int? to string? for Identity FK
|
||||
public string? AssignedUserId { get; set; } // Assigned user
|
||||
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public string? WorkPerformed { get; set; }
|
||||
public string? PartsReplaced { get; set; }
|
||||
|
||||
public decimal LaborCost { get; set; }
|
||||
public decimal PartsCost { get; set; }
|
||||
public decimal TotalCost { get; set; }
|
||||
|
||||
public decimal DowntimeHours { get; set; }
|
||||
|
||||
public string? Notes { get; set; }
|
||||
public string? TechnicianNotes { get; set; }
|
||||
|
||||
// Recurrence
|
||||
public bool IsRecurring { get; set; }
|
||||
public MaintenanceRecurrenceFrequency? RecurrenceFrequency { get; set; }
|
||||
public DateTime? RecurrenceEndDate { get; set; }
|
||||
public string? RecurrenceGroupId { get; set; } // GUID string groups all occurrences in a series
|
||||
public int? RecurrenceParentId { get; set; } // null on parent; child → parent.Id
|
||||
|
||||
// Relationships
|
||||
public virtual Equipment Equipment { get; set; } = null!;
|
||||
public virtual ApplicationUser? PerformedBy { get; set; }
|
||||
public virtual ApplicationUser? AssignedUser { get; set; }
|
||||
public virtual MaintenanceRecord? RecurrenceParent { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using PowderCoating.Core.Enums;
|
||||
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
public class GiftCertificate : BaseEntity
|
||||
{
|
||||
/// <summary>Certificate code shown on the physical/emailed certificate. Format: GC-YYMM-####</summary>
|
||||
public string CertificateCode { get; set; } = string.Empty;
|
||||
|
||||
public decimal OriginalAmount { get; set; }
|
||||
public decimal RedeemedAmount { get; set; }
|
||||
public decimal RemainingBalance => OriginalAmount - RedeemedAmount;
|
||||
|
||||
// Who it's for (optional — may be given to an unknown recipient)
|
||||
public int? RecipientCustomerId { get; set; }
|
||||
public string? RecipientName { get; set; } // Free-text name for non-customers
|
||||
public string? RecipientEmail { get; set; }
|
||||
|
||||
// How it was issued
|
||||
public GiftCertificateIssuedReason IssuedReason { get; set; } = GiftCertificateIssuedReason.Sold;
|
||||
|
||||
// If sold: what the buyer paid (may be less than face value for a promotional sale)
|
||||
public decimal? PurchasePrice { get; set; }
|
||||
public int? PurchasingCustomerId { get; set; }
|
||||
|
||||
public GiftCertificateStatus Status { get; set; } = GiftCertificateStatus.Active;
|
||||
public DateTime IssueDate { get; set; } = DateTime.UtcNow;
|
||||
public DateTime? ExpiryDate { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
public string? IssuedById { get; set; }
|
||||
|
||||
/// <summary>Set when this GC was sold via an invoice line item.</summary>
|
||||
public int? SourceInvoiceItemId { get; set; }
|
||||
|
||||
// Navigation
|
||||
public virtual Customer? RecipientCustomer { get; set; }
|
||||
public virtual Customer? PurchasingCustomer { get; set; }
|
||||
public virtual ApplicationUser? IssuedBy { get; set; }
|
||||
public virtual ICollection<GiftCertificateRedemption> Redemptions { get; set; } = new List<GiftCertificateRedemption>();
|
||||
}
|
||||
|
||||
public class GiftCertificateRedemption : BaseEntity
|
||||
{
|
||||
public int GiftCertificateId { get; set; }
|
||||
public int InvoiceId { get; set; }
|
||||
public decimal AmountRedeemed { get; set; }
|
||||
public DateTime RedeemedDate { get; set; } = DateTime.UtcNow;
|
||||
public string? RedeemedById { get; set; }
|
||||
|
||||
// Navigation
|
||||
public virtual GiftCertificate GiftCertificate { get; set; } = null!;
|
||||
public virtual Invoice Invoice { get; set; } = null!;
|
||||
public virtual ApplicationUser? RedeemedBy { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
public class InAppNotification : BaseEntity
|
||||
{
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Message { get; set; } = string.Empty;
|
||||
public string? Link { get; set; }
|
||||
public string NotificationType { get; set; } = string.Empty; // QuoteApproved, QuoteDeclined, InvoicePaid
|
||||
public bool IsRead { get; set; }
|
||||
public DateTime? ReadAt { get; set; }
|
||||
|
||||
// Optional FK links for context
|
||||
public int? QuoteId { get; set; }
|
||||
public int? InvoiceId { get; set; }
|
||||
public int? CustomerId { get; set; }
|
||||
|
||||
// Navigation
|
||||
public virtual Quote? Quote { get; set; }
|
||||
public virtual Invoice? Invoice { get; set; }
|
||||
public virtual Customer? Customer { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
public class InventoryCategoryLookup : BaseEntity
|
||||
{
|
||||
public string CategoryCode { get; set; } = string.Empty; // Immutable: "POWDER", "PRIMER"
|
||||
public string DisplayName { get; set; } = string.Empty; // Customizable: "Powder Coating"
|
||||
public int DisplayOrder { get; set; } // Sort order in dropdowns
|
||||
public string? Description { get; set; } // Optional description
|
||||
public bool IsActive { get; set; } = true; // Can be disabled
|
||||
public bool IsSystemDefined { get; set; } = true; // Protect from deletion
|
||||
public bool IsCoating { get; set; } = false; // Indicates this category contains coatings that can be applied
|
||||
|
||||
// Relationships
|
||||
public virtual ICollection<InventoryItem> InventoryItems { get; set; } = new List<InventoryItem>();
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
public class InventoryItem : BaseEntity
|
||||
{
|
||||
public string SKU { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
|
||||
// Category (lookup-based)
|
||||
public int? InventoryCategoryId { get; set; } // Nullable during migration
|
||||
public virtual InventoryCategoryLookup? InventoryCategory { get; set; }
|
||||
|
||||
// Legacy field for seed data (will be removed after migration)
|
||||
public string Category { get; set; } = string.Empty;
|
||||
|
||||
// Powder-specific fields
|
||||
public string? ColorName { get; set; }
|
||||
public string? ColorCode { get; set; }
|
||||
public string? Finish { get; set; }
|
||||
public string? Manufacturer { get; set; }
|
||||
public string? ManufacturerPartNumber { get; set; }
|
||||
public decimal? CoverageSqFtPerLb { get; set; } // Square feet coverage per pound (default 30)
|
||||
public decimal? TransferEfficiency { get; set; } // Percentage of powder that sticks (default 65%)
|
||||
public decimal? CureTemperatureF { get; set; } // Required cure temperature in °F (recommended for oven scheduling)
|
||||
public int? CureTimeMinutes { get; set; } // Required hold time at cure temperature
|
||||
public string? ColorFamilies { get; set; } // Comma-separated primary color families e.g. "Green,Blue"
|
||||
public bool RequiresClearCoat { get; set; } // True if this powder requires a clear coat topcoat
|
||||
public string? SpecPageUrl { get; set; } // Link to manufacturer's product/spec page
|
||||
|
||||
// Sample Panel Tracking (coating category items only)
|
||||
public bool HasSamplePanel { get; set; } = false;
|
||||
|
||||
// Inventory Management
|
||||
public decimal QuantityOnHand { get; set; }
|
||||
public string UnitOfMeasure { get; set; } = "lbs"; // lbs, kg, gallons, units, etc.
|
||||
public decimal ReorderPoint { get; set; }
|
||||
public decimal ReorderQuantity { get; set; }
|
||||
public decimal MinimumStock { get; set; }
|
||||
public decimal MaximumStock { get; set; }
|
||||
|
||||
// Pricing
|
||||
public decimal UnitCost { get; set; }
|
||||
public decimal AverageCost { get; set; }
|
||||
public decimal LastPurchasePrice { get; set; }
|
||||
public DateTime? LastPurchaseDate { get; set; }
|
||||
|
||||
// Vendor Information
|
||||
public int? PrimaryVendorId { get; set; }
|
||||
public string? VendorPartNumber { get; set; }
|
||||
|
||||
// Additional fields
|
||||
public string? Location { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
public bool IsActive { get; set; } = true;
|
||||
public DateTime? DiscontinuedDate { get; set; }
|
||||
|
||||
// ── Financial Account Mapping ──────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Asset account where inventory value is tracked on the balance sheet (e.g., 1200 Inventory - Powder).
|
||||
/// When null, falls back to the default inventory asset account.
|
||||
/// </summary>
|
||||
public int? InventoryAccountId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// COGS account debited when this material is consumed on a job (e.g., 5100 Powder & Materials).
|
||||
/// When null, falls back to the default COGS account.
|
||||
/// </summary>
|
||||
public int? CogsAccountId { get; set; }
|
||||
|
||||
// Relationships
|
||||
public virtual Vendor? PrimaryVendor { get; set; }
|
||||
public virtual Account? InventoryAccount { get; set; }
|
||||
public virtual Account? CogsAccount { get; set; }
|
||||
public virtual ICollection<InventoryTransaction> Transactions { get; set; } = new List<InventoryTransaction>();
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
using PowderCoating.Core.Enums;
|
||||
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
public class Invoice : BaseEntity
|
||||
{
|
||||
public string InvoiceNumber { get; set; } = string.Empty;
|
||||
public int? JobId { get; set; }
|
||||
public int CustomerId { get; set; }
|
||||
public string? PreparedById { get; set; }
|
||||
|
||||
public InvoiceStatus Status { get; set; } = InvoiceStatus.Draft;
|
||||
|
||||
// Dates
|
||||
public DateTime InvoiceDate { get; set; } = DateTime.UtcNow;
|
||||
public DateTime? DueDate { get; set; }
|
||||
public DateTime? SentDate { get; set; }
|
||||
public DateTime? PaidDate { get; set; }
|
||||
|
||||
// Financials
|
||||
public decimal SubTotal { get; set; }
|
||||
public decimal TaxPercent { get; set; }
|
||||
public decimal TaxAmount { get; set; }
|
||||
public decimal DiscountAmount { get; set; }
|
||||
public decimal Total { get; set; }
|
||||
public decimal AmountPaid { get; set; }
|
||||
public decimal CreditApplied { get; set; } // Sum of credit memo applications
|
||||
public decimal GiftCertificateRedeemed { get; set; } // Sum of gift certificate redemptions
|
||||
public decimal BalanceDue => Total - AmountPaid - CreditApplied - GiftCertificateRedeemed;
|
||||
|
||||
// Online payments (Stripe Connect)
|
||||
public OnlinePaymentStatus OnlinePaymentStatus { get; set; } = OnlinePaymentStatus.NotApplicable;
|
||||
public string? PaymentLinkToken { get; set; } // Signed token for /pay/{token}
|
||||
public DateTime? PaymentLinkExpiresAt { get; set; } // 5 days from generation
|
||||
public string? StripePaymentIntentId { get; set; } // Most recent PaymentIntent ID
|
||||
public decimal OnlineAmountPaid { get; set; } = 0; // Running total of online payments received
|
||||
public decimal OnlineSurchargeCollected { get; set; } = 0; // Surcharge amount collected (for records)
|
||||
|
||||
// Text
|
||||
public string? Notes { get; set; }
|
||||
public string? InternalNotes { get; set; }
|
||||
public string? Terms { get; set; }
|
||||
public string? CustomerPO { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Original invoice number from an external system (e.g. QuickBooks invoice # "3048").
|
||||
/// Stored for searchability and traceability after import. Searchable from the invoice list.
|
||||
/// </summary>
|
||||
public string? ExternalReference { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Liability account where collected sales tax is tracked (e.g., 2200 Sales Tax Payable).
|
||||
/// Populated automatically when TaxAmount > 0.
|
||||
/// </summary>
|
||||
public int? SalesTaxAccountId { get; set; }
|
||||
|
||||
// Navigation
|
||||
public virtual Job? Job { get; set; }
|
||||
public virtual Customer Customer { get; set; } = null!;
|
||||
public virtual ApplicationUser? PreparedBy { get; set; }
|
||||
public virtual Account? SalesTaxAccount { get; set; }
|
||||
public virtual ICollection<InvoiceItem> InvoiceItems { get; set; } = new List<InvoiceItem>();
|
||||
public virtual ICollection<Payment> Payments { get; set; } = new List<Payment>();
|
||||
public virtual ICollection<Refund> Refunds { get; set; } = new List<Refund>();
|
||||
public virtual ICollection<CreditMemoApplication> CreditApplications { get; set; } = new List<CreditMemoApplication>();
|
||||
public virtual ICollection<GiftCertificateRedemption> GiftCertificateRedemptions { get; set; } = new List<GiftCertificateRedemption>();
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
public class InvoiceItem : BaseEntity
|
||||
{
|
||||
public int InvoiceId { get; set; }
|
||||
public int? SourceJobItemId { get; set; }
|
||||
public int? CatalogItemId { get; set; } // set for merchandise line items
|
||||
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public decimal Quantity { get; set; }
|
||||
public decimal UnitPrice { get; set; }
|
||||
public decimal TotalPrice { get; set; }
|
||||
public string? ColorName { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
public int DisplayOrder { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Revenue account this line item is posted to.
|
||||
/// Pulled from the catalog item at invoice creation time; null falls back to default revenue account.
|
||||
/// </summary>
|
||||
public int? RevenueAccountId { get; set; }
|
||||
|
||||
// Gift certificate sale fields
|
||||
public bool IsGiftCertificate { get; set; } = false;
|
||||
public string? GcRecipientName { get; set; }
|
||||
public string? GcRecipientEmail { get; set; }
|
||||
public DateTime? GcExpiryDate { get; set; }
|
||||
public int? GeneratedGiftCertificateId { get; set; }
|
||||
|
||||
// Navigation
|
||||
public virtual Invoice Invoice { get; set; } = null!;
|
||||
public virtual JobItem? SourceJobItem { get; set; }
|
||||
public virtual CatalogItem? CatalogItem { get; set; }
|
||||
public virtual Account? RevenueAccount { get; set; }
|
||||
public virtual GiftCertificate? GeneratedGiftCertificate { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using PowderCoating.Core.Enums;
|
||||
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
public class Job : BaseEntity
|
||||
{
|
||||
public string JobNumber { get; set; } = string.Empty;
|
||||
public int CustomerId { get; set; }
|
||||
public int? QuoteId { get; set; }
|
||||
public string? AssignedUserId { get; set; } // Assigned user
|
||||
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
// Lookup foreign keys (replacing enums)
|
||||
public int JobStatusId { get; set; }
|
||||
public int JobPriorityId { get; set; }
|
||||
|
||||
// Dates
|
||||
public DateTime? ScheduledDate { get; set; }
|
||||
public DateTime? StartedDate { get; set; }
|
||||
public DateTime? CompletedDate { get; set; }
|
||||
public DateTime? DueDate { get; set; }
|
||||
|
||||
// Selected oven (carried over from quote; null = company default rate)
|
||||
public int? OvenCostId { get; set; }
|
||||
|
||||
// Pricing
|
||||
public decimal QuotedPrice { get; set; }
|
||||
public decimal FinalPrice { get; set; }
|
||||
|
||||
// Discount & rush (mirrors quote fields; preserved through quote→job conversion and job edits)
|
||||
public bool IsRushJob { get; set; }
|
||||
public DiscountType DiscountType { get; set; } = DiscountType.None;
|
||||
public decimal DiscountValue { get; set; }
|
||||
public string? DiscountReason { get; set; }
|
||||
|
||||
// Job Completion Details
|
||||
public decimal? ActualTimeSpentHours { get; set; }
|
||||
|
||||
// Additional Information
|
||||
public string? CustomerPO { get; set; }
|
||||
public string? SpecialInstructions { get; set; }
|
||||
public string? InternalNotes { get; set; } // Internal notes from quote
|
||||
public string? Tags { get; set; }
|
||||
public bool RequiresCustomerApproval { get; set; }
|
||||
public bool IsCustomerApproved { get; set; }
|
||||
|
||||
// Shop floor QR access token (no login required; scoped to this job only)
|
||||
public Guid ShopAccessCode { get; set; } = Guid.NewGuid();
|
||||
|
||||
// Part intake / receiving
|
||||
public DateTime? IntakeDate { get; set; }
|
||||
public string? IntakeConditionNotes { get; set; }
|
||||
public int? IntakePartCount { get; set; }
|
||||
public string? IntakeCheckedByUserId { get; set; }
|
||||
|
||||
// Rework tracking
|
||||
public bool IsReworkJob { get; set; }
|
||||
public int? OriginalJobId { get; set; } // Set when this job was created as a rework
|
||||
|
||||
// Relationships
|
||||
[ForeignKey("IntakeCheckedByUserId")]
|
||||
public virtual ApplicationUser? IntakeCheckedBy { get; set; }
|
||||
public virtual OvenCost? OvenCost { get; set; }
|
||||
public virtual Customer Customer { get; set; } = null!;
|
||||
public virtual Quote? Quote { get; set; }
|
||||
public virtual ApplicationUser? AssignedUser { get; set; }
|
||||
public virtual JobStatusLookup JobStatus { get; set; } = null!;
|
||||
public virtual JobPriorityLookup JobPriority { get; set; } = null!;
|
||||
public virtual ICollection<JobItem> JobItems { get; set; } = new List<JobItem>();
|
||||
public virtual ICollection<JobPhoto> Photos { get; set; } = new List<JobPhoto>();
|
||||
public virtual ICollection<JobNote> Notes { get; set; } = new List<JobNote>();
|
||||
public virtual ICollection<JobStatusHistory> StatusHistory { get; set; } = new List<JobStatusHistory>();
|
||||
public virtual ICollection<JobPrepService> JobPrepServices { get; set; } = new List<JobPrepService>();
|
||||
public virtual Invoice? Invoice { get; set; }
|
||||
public virtual ICollection<JobTimeEntry> TimeEntries { get; set; } = new List<JobTimeEntry>();
|
||||
public virtual ICollection<ReworkRecord> ReworkRecords { get; set; } = new List<ReworkRecord>();
|
||||
public virtual Job? OriginalJob { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
public class JobChangeHistory : BaseEntity
|
||||
{
|
||||
public int JobId { get; set; }
|
||||
public string? ChangedByUserId { get; set; }
|
||||
public DateTime ChangedAt { get; set; } = DateTime.UtcNow;
|
||||
public string FieldName { get; set; } = string.Empty;
|
||||
public string? OldValue { get; set; }
|
||||
public string? NewValue { get; set; }
|
||||
public string ChangeDescription { get; set; } = string.Empty;
|
||||
|
||||
// Navigation properties
|
||||
public Job Job { get; set; } = null!;
|
||||
public ApplicationUser? ChangedBy { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Stores the daily display order for jobs on the Jobs Priority page
|
||||
/// </summary>
|
||||
public class JobDailyPriority : BaseEntity
|
||||
{
|
||||
public int JobId { get; set; }
|
||||
public DateTime ScheduledDate { get; set; }
|
||||
public int DisplayOrder { get; set; }
|
||||
|
||||
// Navigation
|
||||
public virtual Job Job { get; set; } = null!;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
public class JobItem : BaseEntity
|
||||
{
|
||||
public int JobId { get; set; }
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public decimal Quantity { get; set; }
|
||||
|
||||
// Powder/Material Information
|
||||
public string? ColorName { get; set; }
|
||||
public string? ColorCode { get; set; }
|
||||
public string? Finish { get; set; } // Gloss, Matte, Satin, etc.
|
||||
|
||||
// Measurements
|
||||
public decimal? SurfaceArea { get; set; }
|
||||
public decimal SurfaceAreaSqFt { get; set; }
|
||||
|
||||
// Catalog item reference (optional — null for generic/labor items)
|
||||
public int? CatalogItemId { get; set; }
|
||||
|
||||
// Pricing
|
||||
public decimal UnitPrice { get; set; }
|
||||
public decimal TotalPrice { get; set; }
|
||||
public decimal LaborCost { get; set; }
|
||||
public bool IsGenericItem { get; set; }
|
||||
public decimal? ManualUnitPrice { get; set; }
|
||||
public decimal? PowderCostOverride { get; set; } // Optional unit price override for catalog items
|
||||
public bool IsLaborItem { get; set; }
|
||||
public bool IsSalesItem { get; set; }
|
||||
public string? Sku { get; set; }
|
||||
public bool IncludePrepCost { get; set; } = true;
|
||||
|
||||
// Processing Details
|
||||
public bool RequiresSandblasting { get; set; }
|
||||
public bool RequiresMasking { get; set; }
|
||||
public int EstimatedMinutes { get; set; }
|
||||
|
||||
public string? Notes { get; set; }
|
||||
|
||||
// Part complexity level — applies a price multiplier for calculated items
|
||||
// Values: "Simple" | "Moderate" | "Complex" | "Extreme"
|
||||
public string? Complexity { get; set; }
|
||||
|
||||
// AI-generated standardized tags (comma-separated, e.g. "automotive,tubular")
|
||||
public string? AiTags { get; set; }
|
||||
|
||||
// Link to shared AI prediction record (null for non-AI items; shared with QuoteItem when converted)
|
||||
public int? AiPredictionId { get; set; }
|
||||
public virtual AiItemPrediction? AiPrediction { get; set; }
|
||||
|
||||
// Relationships
|
||||
public virtual Job Job { get; set; } = null!;
|
||||
public virtual CatalogItem? CatalogItem { get; set; }
|
||||
public virtual ICollection<JobItemCoat> Coats { get; set; } = new List<JobItemCoat>();
|
||||
public virtual ICollection<JobItemPrepService> PrepServices { get; set; } = new List<JobItemPrepService>();
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a single coating layer applied to a job item.
|
||||
/// Supports multi-coat configurations (primer, base coat, top coat, clear coat, etc.)
|
||||
/// </summary>
|
||||
public class JobItemCoat : BaseEntity
|
||||
{
|
||||
// Parent relationship
|
||||
public int JobItemId { get; set; }
|
||||
|
||||
// Coat identification (user-defined)
|
||||
public string CoatName { get; set; } = string.Empty; // "Primer", "Base Coat", "Top Coat", etc.
|
||||
public int Sequence { get; set; } // 1, 2, 3... for ordering
|
||||
|
||||
// Powder selection (from quote)
|
||||
public int? InventoryItemId { get; set; } // In-stock powder
|
||||
public string? ColorName { get; set; } // Color name
|
||||
public int? VendorId { get; set; } // Vendor for custom powder
|
||||
public string? ColorCode { get; set; } // RAL code, etc.
|
||||
public string? Finish { get; set; } // Gloss, Matte, Textured, etc.
|
||||
|
||||
// Coverage parameters (from quote)
|
||||
public decimal CoverageSqFtPerLb { get; set; } = 30m;
|
||||
public decimal TransferEfficiency { get; set; } = 65m;
|
||||
|
||||
// Cost information (from quote)
|
||||
public decimal? PowderCostPerLb { get; set; } // $/lb
|
||||
public decimal? PowderToOrder { get; set; } // Pounds estimated to order
|
||||
|
||||
// Job completion tracking
|
||||
public decimal? ActualPowderUsedLbs { get; set; } // Actual powder used when job is completed
|
||||
|
||||
// Powder ordering tracking
|
||||
public bool PowderOrdered { get; set; } = false;
|
||||
public DateTime? PowderOrderedAt { get; set; }
|
||||
public string? PowderOrderedByUserId { get; set; }
|
||||
|
||||
// Powder receiving tracking
|
||||
public bool PowderReceived { get; set; } = false;
|
||||
public DateTime? PowderReceivedAt { get; set; }
|
||||
public string? PowderReceivedByUserId { get; set; }
|
||||
public decimal? PowderReceivedLbs { get; set; }
|
||||
|
||||
// Notes
|
||||
public string? Notes { get; set; }
|
||||
|
||||
// Navigation properties
|
||||
public virtual JobItem JobItem { get; set; } = null!;
|
||||
public virtual InventoryItem? InventoryItem { get; set; }
|
||||
public virtual Vendor? Vendor { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
public class JobItemPrepService : BaseEntity
|
||||
{
|
||||
public int JobItemId { get; set; }
|
||||
public int PrepServiceId { get; set; }
|
||||
public int EstimatedMinutes { get; set; }
|
||||
|
||||
/// <summary>Which blast setup was selected when this sandblasting prep service was added.</summary>
|
||||
public int? BlastSetupId { get; set; }
|
||||
|
||||
public virtual JobItem JobItem { get; set; } = null!;
|
||||
public virtual PrepService PrepService { get; set; } = null!;
|
||||
public virtual CompanyBlastSetup? BlastSetup { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace PowderCoating.Core.Entities
|
||||
{
|
||||
/// <summary>
|
||||
/// Join table for the many-to-many relationship between Jobs and PrepServices
|
||||
/// </summary>
|
||||
public class JobPrepService : BaseEntity
|
||||
{
|
||||
public int JobId { get; set; }
|
||||
public int PrepServiceId { get; set; }
|
||||
|
||||
// Navigation properties
|
||||
[ForeignKey(nameof(JobId))]
|
||||
public virtual Job Job { get; set; } = null!;
|
||||
|
||||
[ForeignKey(nameof(PrepServiceId))]
|
||||
public virtual PrepService PrepService { get; set; } = null!;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Company-specific job priority lookup table.
|
||||
/// Replaces hardcoded JobPriority enum to enable priority customization.
|
||||
/// </summary>
|
||||
public class JobPriorityLookup : BaseEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Immutable priority code used in code logic (e.g., "LOW", "NORMAL", "URGENT", "RUSH").
|
||||
/// Acts like the old enum name.
|
||||
/// </summary>
|
||||
public string PriorityCode { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// User-customizable display name shown in UI (e.g., "Standard", "Expedited", "Emergency").
|
||||
/// </summary>
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Priority level for sorting (1 = lowest priority, 5 = highest priority).
|
||||
/// Replaces enum integer comparisons.
|
||||
/// </summary>
|
||||
public int DisplayOrder { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Bootstrap color class for badge styling (primary, secondary, success, danger, warning, info, dark).
|
||||
/// Eliminates switch statement duplication for priority colors.
|
||||
/// </summary>
|
||||
public string ColorClass { get; set; } = "secondary";
|
||||
|
||||
/// <summary>
|
||||
/// Optional Bootstrap icon class (e.g., "bi-arrow-up", "bi-exclamation-triangle").
|
||||
/// </summary>
|
||||
public string? IconClass { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this priority is currently active and available for selection.
|
||||
/// </summary>
|
||||
public bool IsActive { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// System-defined priorities cannot be deleted.
|
||||
/// </summary>
|
||||
public bool IsSystemDefined { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Optional description explaining when to use this priority level.
|
||||
/// </summary>
|
||||
public string? Description { get; set; }
|
||||
|
||||
// Navigation properties
|
||||
public virtual ICollection<Job> Jobs { get; set; } = new List<Job>();
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Company-specific job status lookup table.
|
||||
/// Replaces hardcoded JobStatus enum to enable workflow customization.
|
||||
/// </summary>
|
||||
public class JobStatusLookup : BaseEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Immutable status code used in code logic (e.g., "PENDING", "COATING", "COMPLETED").
|
||||
/// Acts like the old enum name.
|
||||
/// </summary>
|
||||
public string StatusCode { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// User-customizable display name shown in UI (e.g., "In Progress", "Ready for Coating").
|
||||
/// </summary>
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Workflow sequence number for ordering statuses (1 = first, higher = later in workflow).
|
||||
/// Replaces enum integer comparisons.
|
||||
/// </summary>
|
||||
public int DisplayOrder { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Bootstrap color class for badge styling (primary, secondary, success, danger, warning, info, dark).
|
||||
/// Eliminates switch statement duplication for status colors.
|
||||
/// </summary>
|
||||
public string ColorClass { get; set; } = "secondary";
|
||||
|
||||
/// <summary>
|
||||
/// Optional Bootstrap icon class (e.g., "bi-clock", "bi-paint-bucket", "bi-check-circle").
|
||||
/// </summary>
|
||||
public string? IconClass { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this status is currently active and available for selection.
|
||||
/// </summary>
|
||||
public bool IsActive { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// System-defined statuses cannot be deleted (PENDING, COMPLETED, CANCELLED).
|
||||
/// Prevents breaking workflow dependencies.
|
||||
/// </summary>
|
||||
public bool IsSystemDefined { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Terminal statuses represent completed workflows (COMPLETED, DELIVERED, CANCELLED).
|
||||
/// Used for reporting and filtering.
|
||||
/// </summary>
|
||||
public bool IsTerminalStatus { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Work-in-progress statuses represent active production (SANDBLASTING, COATING, CURING, etc.).
|
||||
/// Used for dashboard stats and capacity planning.
|
||||
/// </summary>
|
||||
public bool IsWorkInProgressStatus { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Optional description explaining when to use this status.
|
||||
/// </summary>
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional workflow category for grouping statuses (e.g., "Pre-Production", "Production", "Post-Production").
|
||||
/// </summary>
|
||||
public string? WorkflowCategory { get; set; }
|
||||
|
||||
// Navigation properties
|
||||
public virtual ICollection<Job> Jobs { get; set; } = new List<Job>();
|
||||
public virtual ICollection<JobStatusHistory> FromStatusHistory { get; set; } = new List<JobStatusHistory>();
|
||||
public virtual ICollection<JobStatusHistory> ToStatusHistory { get; set; } = new List<JobStatusHistory>();
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// A saved job configuration that can be reused to quickly create new jobs.
|
||||
/// </summary>
|
||||
public class JobTemplate : BaseEntity
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
public int? CustomerId { get; set; }
|
||||
public string? SpecialInstructions { get; set; }
|
||||
public bool IsActive { get; set; } = true;
|
||||
public int UsageCount { get; set; } = 0;
|
||||
|
||||
// Navigation properties
|
||||
public virtual Customer? Customer { get; set; }
|
||||
public virtual ICollection<JobTemplateItem> Items { get; set; } = new List<JobTemplateItem>();
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
public class JobTemplateItem : BaseEntity
|
||||
{
|
||||
public int JobTemplateId { get; set; }
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public decimal Quantity { get; set; } = 1;
|
||||
public decimal SurfaceAreaSqFt { get; set; }
|
||||
public int? CatalogItemId { get; set; }
|
||||
public bool IsGenericItem { get; set; }
|
||||
public bool IsLaborItem { get; set; }
|
||||
public decimal? ManualUnitPrice { get; set; }
|
||||
public bool RequiresSandblasting { get; set; }
|
||||
public bool RequiresMasking { get; set; }
|
||||
public bool IncludePrepCost { get; set; }
|
||||
public int EstimatedMinutes { get; set; }
|
||||
public string? Complexity { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
public int DisplayOrder { get; set; }
|
||||
|
||||
// Navigation properties
|
||||
public virtual JobTemplate JobTemplate { get; set; } = null!;
|
||||
public virtual CatalogItem? CatalogItem { get; set; }
|
||||
public virtual ICollection<JobTemplateItemCoat> Coats { get; set; } = new List<JobTemplateItemCoat>();
|
||||
public virtual ICollection<JobTemplateItemPrepService> PrepServices { get; set; } = new List<JobTemplateItemPrepService>();
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
public class JobTemplateItemCoat : BaseEntity
|
||||
{
|
||||
public int JobTemplateItemId { get; set; }
|
||||
public string CoatName { get; set; } = string.Empty;
|
||||
public int Sequence { get; set; }
|
||||
public int? InventoryItemId { get; set; }
|
||||
public string? ColorName { get; set; }
|
||||
public int? VendorId { get; set; }
|
||||
public string? ColorCode { get; set; }
|
||||
public string? Finish { get; set; }
|
||||
public decimal CoverageSqFtPerLb { get; set; } = 30m;
|
||||
public decimal TransferEfficiency { get; set; } = 65m;
|
||||
public decimal? PowderCostPerLb { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
|
||||
// Navigation properties
|
||||
public virtual JobTemplateItem JobTemplateItem { get; set; } = null!;
|
||||
public virtual InventoryItem? InventoryItem { get; set; }
|
||||
public virtual Vendor? Vendor { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
public class JobTemplateItemPrepService : BaseEntity
|
||||
{
|
||||
public int JobTemplateItemId { get; set; }
|
||||
public int PrepServiceId { get; set; }
|
||||
public int EstimatedMinutes { get; set; }
|
||||
|
||||
// Navigation properties
|
||||
public virtual JobTemplateItem JobTemplateItem { get; set; } = null!;
|
||||
public virtual PrepService PrepService { get; set; } = null!;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
public class JobTimeEntry : BaseEntity
|
||||
{
|
||||
public int JobId { get; set; }
|
||||
public int ShopWorkerId { get; set; }
|
||||
public DateTime WorkDate { get; set; }
|
||||
public decimal HoursWorked { get; set; }
|
||||
public string? Stage { get; set; } // e.g. "Sandblasting", "Coating", "Masking" — free text
|
||||
public string? Notes { get; set; }
|
||||
|
||||
// Navigation
|
||||
public virtual Job Job { get; set; } = null!;
|
||||
public virtual ShopWorker Worker { get; set; } = null!;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Stores per-manufacturer URL patterns so the AI lookup service can build
|
||||
/// direct product page URLs rather than relying solely on search results.
|
||||
/// These records are global (CompanyId = 0) and shared across all tenants.
|
||||
/// </summary>
|
||||
public class ManufacturerLookupPattern : BaseEntity
|
||||
{
|
||||
/// <summary>Display / match name (case-insensitive contains check).</summary>
|
||||
public string ManufacturerName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// URL template. Supported placeholders:
|
||||
/// {partNumber} – manufacturer part number (slashes normalized to hyphens)
|
||||
/// {slug} – color name transformed by SlugTransform
|
||||
/// {colorCode} – color code as-is
|
||||
/// If a required placeholder is missing at runtime the template is skipped and
|
||||
/// the system falls back to a Serper search URL.
|
||||
/// </summary>
|
||||
public string? ProductUrlTemplate { get; set; }
|
||||
|
||||
/// <summary>How the color name is turned into a URL slug.</summary>
|
||||
public string SlugTransform { get; set; } = "LowerHyphen"; // LowerHyphen | LowerUnderscore | TitleHyphen | AsIs
|
||||
|
||||
/// <summary>
|
||||
/// Manufacturer/retailer domain (e.g. "prismaticpowders.com").
|
||||
/// Used by the search result URL picker even when no template is configured.
|
||||
/// </summary>
|
||||
public string? Domain { get; set; }
|
||||
|
||||
public bool IsActive { get; set; } = true;
|
||||
public string? Notes { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using PowderCoating.Core.Enums;
|
||||
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
public class NotificationLog : BaseEntity
|
||||
{
|
||||
public NotificationChannel Channel { get; set; }
|
||||
public NotificationType NotificationType { get; set; }
|
||||
public NotificationStatus Status { get; set; }
|
||||
public string RecipientName { get; set; } = string.Empty;
|
||||
public string Recipient { get; set; } = string.Empty; // email address or phone number
|
||||
public string? Subject { get; set; }
|
||||
public string Message { get; set; } = string.Empty;
|
||||
public string? ErrorMessage { get; set; }
|
||||
public DateTime SentAt { get; set; }
|
||||
|
||||
// Optional FK links
|
||||
public int? CustomerId { get; set; }
|
||||
public int? JobId { get; set; }
|
||||
public int? QuoteId { get; set; }
|
||||
public int? InvoiceId { get; set; }
|
||||
|
||||
// Navigation properties
|
||||
public virtual Customer? Customer { get; set; }
|
||||
public virtual Job? Job { get; set; }
|
||||
public virtual Quote? Quote { get; set; }
|
||||
public virtual Invoice? Invoice { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using PowderCoating.Core.Enums;
|
||||
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
public class NotificationTemplate : BaseEntity
|
||||
{
|
||||
public NotificationType NotificationType { get; set; }
|
||||
public NotificationChannel Channel { get; set; }
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
public string? Subject { get; set; } // null for SMS
|
||||
public string Body { get; set; } = string.Empty; // HTML for Email, plain text for SMS
|
||||
public bool IsActive { get; set; } = true;
|
||||
public virtual Company Company { get; set; } = null!;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using PowderCoating.Core.Enums;
|
||||
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
public class OvenBatch : BaseEntity
|
||||
{
|
||||
public string BatchNumber { get; set; } = string.Empty;
|
||||
public int? EquipmentId { get; set; }
|
||||
public int? OvenCostId { get; set; }
|
||||
public OvenBatchStatus Status { get; set; } = OvenBatchStatus.Planned;
|
||||
|
||||
public DateTime ScheduledDate { get; set; }
|
||||
public DateTime? ScheduledStartTime { get; set; }
|
||||
public DateTime? EstimatedEndTime { get; set; }
|
||||
public DateTime? ActualStartTime { get; set; }
|
||||
public DateTime? ActualEndTime { get; set; }
|
||||
|
||||
public decimal TotalSurfaceAreaSqFt { get; set; }
|
||||
public decimal? CureTemperatureF { get; set; }
|
||||
public int CycleMinutes { get; set; } = 45;
|
||||
|
||||
public string? PrimaryColorName { get; set; }
|
||||
public string? PrimaryColorCode { get; set; }
|
||||
|
||||
public bool AiSuggested { get; set; }
|
||||
public string? AiReasoningJson { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
|
||||
// Navigation
|
||||
public virtual Equipment? Equipment { get; set; }
|
||||
public virtual OvenCost? OvenCost { get; set; }
|
||||
public virtual ICollection<OvenBatchItem> Items { get; set; } = new List<OvenBatchItem>();
|
||||
}
|
||||
|
||||
public class OvenBatchItem : BaseEntity
|
||||
{
|
||||
public int OvenBatchId { get; set; }
|
||||
public int JobId { get; set; }
|
||||
public int JobItemId { get; set; }
|
||||
public int JobItemCoatId { get; set; }
|
||||
|
||||
public decimal SurfaceAreaContribution { get; set; }
|
||||
public int CoatPassNumber { get; set; } = 1;
|
||||
public int SortOrder { get; set; }
|
||||
public OvenBatchItemStatus Status { get; set; } = OvenBatchItemStatus.Pending;
|
||||
public string? Notes { get; set; }
|
||||
|
||||
// Navigation
|
||||
public virtual OvenBatch Batch { get; set; } = null!;
|
||||
public virtual Job Job { get; set; } = null!;
|
||||
public virtual JobItem JobItem { get; set; } = null!;
|
||||
public virtual JobItemCoat JobItemCoat { get; set; } = null!;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
public class OvenCost : BaseEntity
|
||||
{
|
||||
[Required]
|
||||
[StringLength(100)]
|
||||
public string Label { get; set; } = string.Empty;
|
||||
|
||||
[Range(0, 10000)]
|
||||
public decimal CostPerHour { get; set; }
|
||||
|
||||
public bool IsActive { get; set; } = true;
|
||||
|
||||
public int DisplayOrder { get; set; } = 0;
|
||||
|
||||
/// <summary>Maximum load capacity in square feet for the Oven Scheduler.</summary>
|
||||
[Range(0, 100000)]
|
||||
public decimal? MaxLoadSqFt { get; set; }
|
||||
|
||||
/// <summary>Default cure cycle duration in minutes for the Oven Scheduler.</summary>
|
||||
[Range(1, 1440)]
|
||||
public int? DefaultCycleMinutes { get; set; }
|
||||
|
||||
// Navigation
|
||||
public virtual Company Company { get; set; } = null!;
|
||||
public virtual ICollection<Quote> Quotes { get; set; } = new List<Quote>();
|
||||
public virtual ICollection<Job> Jobs { get; set; } = new List<Job>();
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using PowderCoating.Core.Enums;
|
||||
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
public class Payment : BaseEntity
|
||||
{
|
||||
public int InvoiceId { get; set; }
|
||||
public decimal Amount { get; set; }
|
||||
public DateTime PaymentDate { get; set; } = DateTime.UtcNow;
|
||||
public PaymentMethod PaymentMethod { get; set; }
|
||||
public string? Reference { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
public string? RecordedById { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Bank/checking account the payment is deposited into.
|
||||
/// When null, no specific deposit account is tracked.
|
||||
/// </summary>
|
||||
public int? DepositAccountId { get; set; }
|
||||
|
||||
// Navigation
|
||||
public virtual Invoice Invoice { get; set; } = null!;
|
||||
public virtual ApplicationUser? RecordedBy { get; set; }
|
||||
public virtual Account? DepositAccount { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Temporarily stores registration form data while the user completes a Stripe checkout.
|
||||
/// Keyed by a GUID token that is embedded in the Stripe success/cancel URLs so no session
|
||||
/// cookie is required to survive the cross-origin redirect.
|
||||
/// Does NOT inherit BaseEntity — no CompanyId, no soft-delete, no tenant filter.
|
||||
/// </summary>
|
||||
public class PendingRegistrationSession
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>GUID token embedded in the Stripe success/cancel URLs.</summary>
|
||||
public string Token { get; set; } = string.Empty;
|
||||
|
||||
public string CompanyName { get; set; } = string.Empty;
|
||||
public string? CompanyPhone { get; set; }
|
||||
public string FirstName { get; set; } = string.Empty;
|
||||
public string LastName { get; set; } = string.Empty;
|
||||
public string Email { get; set; } = string.Empty;
|
||||
public int Plan { get; set; }
|
||||
public bool IsAnnual { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
/// <summary>Set to true once the registration is successfully completed.</summary>
|
||||
public bool IsCompleted { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Platform-wide key/value settings managed via the SuperAdmin UI.
|
||||
/// Intentionally does NOT inherit BaseEntity — no CompanyId, no soft-delete, no tenant filter.
|
||||
/// </summary>
|
||||
public class PlatformSetting
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>Unique string key, e.g. "AdminNotificationEmail"</summary>
|
||||
[MaxLength(200)]
|
||||
public string Key { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>The stored value (null = unset/use default)</summary>
|
||||
public string? Value { get; set; }
|
||||
|
||||
/// <summary>Human-readable label shown in the Platform Settings UI</summary>
|
||||
public string? Label { get; set; }
|
||||
|
||||
/// <summary>Explanation shown below the field in the UI</summary>
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>Groups related settings together in the UI (e.g. "Notifications", "Branding")</summary>
|
||||
public string? GroupName { get; set; }
|
||||
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
public string? UpdatedBy { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Well-known keys for the PlatformSettings table.
|
||||
/// </summary>
|
||||
public static class PlatformSettingKeys
|
||||
{
|
||||
public const string AdminNotificationEmail = "AdminNotificationEmail";
|
||||
public const string BaseUrl = "BaseUrl";
|
||||
public const string TrialPeriodDays = "TrialPeriodDays";
|
||||
public const string TrialsEnabled = "TrialsEnabled";
|
||||
public const string QuoteApprovalTokenDays = "QuoteApprovalTokenDays";
|
||||
public const string AuditLogRetentionDays = "AuditLogRetentionDays";
|
||||
public const string StripeWebhookRetentionDays = "StripeWebhookRetentionDays";
|
||||
public const string MaxTenants = "MaxTenants";
|
||||
public const string SmsEnabled = "SmsEnabled";
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Records actual powder consumption against a specific coat on a job.
|
||||
/// Links the InventoryTransaction (the deduction) to the JobItemCoat (what the powder was used on).
|
||||
/// This is the bridge that enables prediction vs actual reporting per powder SKU.
|
||||
/// </summary>
|
||||
public class PowderUsageLog : BaseEntity
|
||||
{
|
||||
public int JobId { get; set; }
|
||||
public int JobItemId { get; set; }
|
||||
public int JobItemCoatId { get; set; }
|
||||
public int? InventoryItemId { get; set; } // null for custom/vendor powder
|
||||
public int? InventoryTransactionId { get; set; } // FK to the inventory deduction (if auto-linked)
|
||||
|
||||
public decimal ActualLbsUsed { get; set; }
|
||||
public decimal EstimatedLbs { get; set; } // Snapshot of PowderToOrder at time of recording
|
||||
public decimal VarianceLbs { get; set; } // ActualLbsUsed - EstimatedLbs (positive = used more than estimated)
|
||||
|
||||
public string RecordedByUserId { get; set; } = string.Empty;
|
||||
public DateTime RecordedAt { get; set; } = DateTime.UtcNow;
|
||||
public string? Notes { get; set; }
|
||||
|
||||
// Navigation
|
||||
public virtual Job Job { get; set; } = null!;
|
||||
public virtual JobItem JobItem { get; set; } = null!;
|
||||
public virtual JobItemCoat JobItemCoat { get; set; } = null!;
|
||||
public virtual InventoryItem? InventoryItem { get; set; }
|
||||
public virtual InventoryTransaction? InventoryTransaction { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Company-specific preparation service lookup table.
|
||||
/// Stores common prep services like "Sandblasting", "Chemical Stripping", "Hand Sanding", etc.
|
||||
/// </summary>
|
||||
public class PrepService : BaseEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Name of the preparation service (e.g., "Sandblasting", "Chemical Stripping").
|
||||
/// </summary>
|
||||
public string ServiceName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Optional detailed description of the service.
|
||||
/// </summary>
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Display order for sorting services in lists (lower numbers appear first).
|
||||
/// </summary>
|
||||
public int DisplayOrder { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this service is currently active and available for selection.
|
||||
/// </summary>
|
||||
public bool IsActive { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// When true, the item wizard shows a blast-setup selector for this prep service so the
|
||||
/// correct throughput rate is used to calculate estimated minutes.
|
||||
/// </summary>
|
||||
public bool RequiresBlastSetup { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using PowderCoating.Core.Enums;
|
||||
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
public class PurchaseOrder : BaseEntity
|
||||
{
|
||||
public string PoNumber { get; set; } = string.Empty;
|
||||
|
||||
public int VendorId { get; set; }
|
||||
public virtual Vendor Vendor { get; set; } = null!;
|
||||
|
||||
public PurchaseOrderStatus Status { get; set; } = PurchaseOrderStatus.Draft;
|
||||
|
||||
public DateTime OrderDate { get; set; } = DateTime.UtcNow;
|
||||
public DateTime? ExpectedDeliveryDate { get; set; }
|
||||
public DateTime? ReceivedDate { get; set; }
|
||||
|
||||
public decimal ShippingCost { get; set; } = 0;
|
||||
public decimal SubTotal { get; set; } = 0;
|
||||
public decimal TotalAmount { get; set; } = 0;
|
||||
|
||||
public string? Notes { get; set; }
|
||||
public string? InternalNotes { get; set; }
|
||||
|
||||
// Optional link to a Bill created after receipt
|
||||
public int? BillId { get; set; }
|
||||
public virtual Bill? Bill { get; set; }
|
||||
|
||||
public virtual ICollection<PurchaseOrderItem> Items { get; set; } = new List<PurchaseOrderItem>();
|
||||
}
|
||||
|
||||
public class PurchaseOrderItem : BaseEntity
|
||||
{
|
||||
public int PurchaseOrderId { get; set; }
|
||||
public virtual PurchaseOrder PurchaseOrder { get; set; } = null!;
|
||||
|
||||
// Null for custom/non-inventory line items
|
||||
public int? InventoryItemId { get; set; }
|
||||
public virtual InventoryItem? InventoryItem { get; set; }
|
||||
|
||||
// Used when InventoryItemId is null
|
||||
public string? Description { get; set; }
|
||||
public string? UnitOfMeasure { get; set; }
|
||||
|
||||
public decimal QuantityOrdered { get; set; }
|
||||
public decimal QuantityReceived { get; set; } = 0;
|
||||
public decimal UnitCost { get; set; }
|
||||
public decimal LineTotal { get; set; }
|
||||
|
||||
public string? Notes { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
using PowderCoating.Core.Enums;
|
||||
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
public class Quote : BaseEntity
|
||||
{
|
||||
public string QuoteNumber { get; set; } = string.Empty;
|
||||
public int? CustomerId { get; set; } // Nullable to support quotes for prospects
|
||||
public string? PreparedById { get; set; } // Changed from int? to string? for Identity FK
|
||||
|
||||
// Prospect Contact Information (used when CustomerId is null)
|
||||
public string? ProspectCompanyName { get; set; }
|
||||
public string? ProspectContactName { get; set; }
|
||||
public string? ProspectEmail { get; set; }
|
||||
public string? ProspectPhone { get; set; }
|
||||
public string? ProspectAddress { get; set; }
|
||||
public string? ProspectCity { get; set; }
|
||||
public string? ProspectState { get; set; }
|
||||
public string? ProspectZipCode { get; set; }
|
||||
|
||||
// Lookup foreign key (replacing enum)
|
||||
public int QuoteStatusId { get; set; }
|
||||
|
||||
// Selected oven for this quote (null = use company default rate)
|
||||
public int? OvenCostId { get; set; }
|
||||
|
||||
// Oven batch pricing
|
||||
public int OvenBatches { get; set; } = 1;
|
||||
public int? OvenCycleMinutes { get; set; } // null = use company DefaultOvenCycleMinutes
|
||||
public bool IsCommercial { get; set; }
|
||||
public bool IsRushJob { get; set; } = false;
|
||||
|
||||
// Dates
|
||||
public DateTime QuoteDate { get; set; } = DateTime.UtcNow;
|
||||
public DateTime? ExpirationDate { get; set; }
|
||||
public DateTime? SentDate { get; set; }
|
||||
public DateTime? ApprovedDate { get; set; }
|
||||
|
||||
// Pricing — all values are snapshots captured at save time and must not be recalculated on load
|
||||
public decimal MaterialCosts { get; set; } // Sum of powder/material costs across all items
|
||||
public decimal LaborCosts { get; set; } // Sum of labor costs across all items
|
||||
public decimal EquipmentCosts { get; set; } // Sum of equipment costs across all items
|
||||
public decimal ItemsSubtotal { get; set; } // Sum of item prices before any quote-level costs
|
||||
public decimal OvenBatchCost { get; set; } // Oven batch charge applied at quote level
|
||||
public decimal ShopSuppliesAmount { get; set; } // Shop supplies dollar amount
|
||||
public decimal ShopSuppliesPercent { get; set; } // Shop supplies percentage used
|
||||
public decimal OverheadAmount { get; set; } // Overhead dollar amount
|
||||
public decimal OverheadPercent { get; set; } // Overhead percentage used
|
||||
public decimal ProfitMargin { get; set; } // Profit margin dollar amount
|
||||
public decimal ProfitPercent { get; set; } // Profit margin percentage used
|
||||
public decimal SubTotal { get; set; } // SubtotalBeforeDiscount (items + oven + overhead + profit + shop supplies)
|
||||
|
||||
// Discount Information
|
||||
public DiscountType DiscountType { get; set; } = DiscountType.None;
|
||||
public decimal DiscountValue { get; set; } = 0; // Value entered by user (percentage or fixed amount)
|
||||
public decimal DiscountPercent { get; set; } // Calculated: actual percentage applied
|
||||
public decimal DiscountAmount { get; set; } // Calculated: actual dollar amount deducted
|
||||
public string? DiscountReason { get; set; } // Why discount was applied
|
||||
public bool HideDiscountFromCustomer { get; set; } = false; // Show only total on PDFs/portal
|
||||
|
||||
public decimal TaxPercent { get; set; }
|
||||
public decimal TaxAmount { get; set; }
|
||||
public decimal RushFee { get; set; } = 0;
|
||||
public decimal Total { get; set; }
|
||||
|
||||
// Deposit — require deposit payment before work begins
|
||||
public bool RequiresDeposit { get; set; } = false;
|
||||
public decimal DepositPercent { get; set; } = 0; // e.g. 50 = 50% deposit required
|
||||
|
||||
// Online deposit payment (Stripe Connect)
|
||||
public string? DepositPaymentLinkToken { get; set; }
|
||||
public DateTime? DepositPaymentLinkExpiresAt { get; set; }
|
||||
public decimal DepositAmountPaid { get; set; } = 0;
|
||||
public string? DepositPaymentIntentId { get; set; }
|
||||
|
||||
// Additional Information
|
||||
public string? Description { get; set; }
|
||||
public string? Terms { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
public string? CustomerPO { get; set; }
|
||||
public string? Tags { get; set; }
|
||||
|
||||
// Conversion tracking
|
||||
public int? ConvertedToJobId { get; set; }
|
||||
public DateTime? ConvertedDate { get; set; }
|
||||
|
||||
// Customer self-service approval
|
||||
public string? ApprovalToken { get; set; }
|
||||
public DateTime? ApprovalTokenExpiresAt { get; set; }
|
||||
public DateTime? ApprovalTokenUsedAt { get; set; }
|
||||
public string? DeclineReason { get; set; }
|
||||
public string? DeclinedByIp { get; set; }
|
||||
|
||||
// Relationships
|
||||
public virtual OvenCost? OvenCost { get; set; }
|
||||
public virtual Customer? Customer { get; set; }
|
||||
public virtual ApplicationUser? PreparedBy { get; set; }
|
||||
public virtual QuoteStatusLookup QuoteStatus { get; set; } = null!;
|
||||
public virtual ICollection<QuoteItem> QuoteItems { get; set; } = new List<QuoteItem>();
|
||||
public virtual ICollection<QuotePrepService> QuotePrepServices { get; set; } = new List<QuotePrepService>();
|
||||
public virtual Job? ConvertedToJob { get; set; }
|
||||
public virtual ICollection<QuotePhoto> QuotePhotos { get; set; } = new List<QuotePhoto>();
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
public class QuoteChangeHistory : BaseEntity
|
||||
{
|
||||
public int QuoteId { get; set; }
|
||||
public string? ChangedByUserId { get; set; }
|
||||
public DateTime ChangedAt { get; set; } = DateTime.UtcNow;
|
||||
public string FieldName { get; set; } = string.Empty;
|
||||
public string? OldValue { get; set; }
|
||||
public string? NewValue { get; set; }
|
||||
public string ChangeDescription { get; set; } = string.Empty;
|
||||
|
||||
// Navigation properties
|
||||
public Quote Quote { get; set; } = null!;
|
||||
public ApplicationUser? ChangedBy { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
public class QuoteItem : BaseEntity
|
||||
{
|
||||
public int QuoteId { get; set; }
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public decimal Quantity { get; set; }
|
||||
|
||||
// Measurements (optional for quoting)
|
||||
public decimal? SurfaceArea { get; set; }
|
||||
public decimal SurfaceAreaSqFt { get; set; } // Surface area in square feet for pricing
|
||||
|
||||
// Catalog item reference (optional)
|
||||
public int? CatalogItemId { get; set; } // Link to catalog item (optional)
|
||||
|
||||
// Pricing
|
||||
public decimal UnitPrice { get; set; }
|
||||
public decimal TotalPrice { get; set; }
|
||||
|
||||
// Cost breakdown snapshot (set at save time for breakdown display)
|
||||
public decimal ItemMaterialCost { get; set; }
|
||||
public decimal ItemLaborCost { get; set; }
|
||||
public decimal ItemEquipmentCost { get; set; }
|
||||
public bool IsGenericItem { get; set; }
|
||||
public decimal? ManualUnitPrice { get; set; }
|
||||
public decimal? PowderCostOverride { get; set; } // Optional unit price override for catalog items
|
||||
public bool IsLaborItem { get; set; }
|
||||
public bool IsSalesItem { get; set; }
|
||||
public string? Sku { get; set; }
|
||||
|
||||
// Processing estimates
|
||||
public bool RequiresSandblasting { get; set; }
|
||||
public bool RequiresMasking { get; set; }
|
||||
public int EstimatedMinutes { get; set; }
|
||||
|
||||
public string? Notes { get; set; }
|
||||
|
||||
// Part complexity level — applies a price multiplier for calculated items
|
||||
// Values: "Simple" | "Moderate" | "Complex" | "Extreme"
|
||||
public string? Complexity { get; set; }
|
||||
|
||||
// True when this item was generated via AI photo analysis.
|
||||
// Must be persisted so that recalculations (Details view, Edit view) can
|
||||
// honour ManualUnitPrice instead of running the regular pricing engine.
|
||||
public bool IsAiItem { get; set; }
|
||||
|
||||
// AI-generated standardized tags (comma-separated, e.g. "automotive,tubular")
|
||||
public string? AiTags { get; set; }
|
||||
|
||||
// Link to shared AI prediction record (null for non-AI items)
|
||||
public int? AiPredictionId { get; set; }
|
||||
public virtual AiItemPrediction? AiPrediction { get; set; }
|
||||
|
||||
// Relationships
|
||||
public virtual Quote Quote { get; set; } = null!;
|
||||
public virtual CatalogItem? CatalogItem { get; set; }
|
||||
public virtual ICollection<QuoteItemCoat> Coats { get; set; } = new List<QuoteItemCoat>();
|
||||
public virtual ICollection<QuoteItemPrepService> PrepServices { get; set; } = new List<QuoteItemPrepService>();
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a single coating layer applied to a quote item.
|
||||
/// Supports multi-coat configurations (primer, base coat, top coat, clear coat, etc.)
|
||||
/// </summary>
|
||||
public class QuoteItemCoat : BaseEntity
|
||||
{
|
||||
// Parent relationship
|
||||
public int QuoteItemId { get; set; }
|
||||
|
||||
// Coat identification (user-defined)
|
||||
public string CoatName { get; set; } = string.Empty; // "Primer", "Base Coat", "Top Coat", etc.
|
||||
public int Sequence { get; set; } // 1, 2, 3... for ordering
|
||||
|
||||
// Powder selection (same pattern as current QuoteItem)
|
||||
public int? InventoryItemId { get; set; } // In-stock powder
|
||||
public string? ColorName { get; set; } // Color name
|
||||
public int? VendorId { get; set; } // Vendor for custom powder
|
||||
public string? ColorCode { get; set; } // RAL code, etc.
|
||||
public string? Finish { get; set; } // Gloss, Matte, Textured, etc.
|
||||
|
||||
// Coverage parameters (defaults from inventory or user-specified)
|
||||
public decimal CoverageSqFtPerLb { get; set; } = 30m;
|
||||
public decimal TransferEfficiency { get; set; } = 65m;
|
||||
|
||||
// Cost override for custom powder
|
||||
public decimal? PowderCostPerLb { get; set; } // $/lb for custom orders
|
||||
public decimal? PowderToOrder { get; set; } // Pounds to order (rounded up from needed)
|
||||
|
||||
// Calculated costs (stored for audit trail)
|
||||
public decimal CoatMaterialCost { get; set; }
|
||||
public decimal CoatLaborCost { get; set; }
|
||||
public decimal CoatTotalCost { get; set; }
|
||||
|
||||
// Notes
|
||||
public string? Notes { get; set; }
|
||||
|
||||
// Navigation properties
|
||||
public virtual QuoteItem QuoteItem { get; set; } = null!;
|
||||
public virtual InventoryItem? InventoryItem { get; set; }
|
||||
public virtual Vendor? Vendor { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
public class QuoteItemPrepService : BaseEntity
|
||||
{
|
||||
public int QuoteItemId { get; set; }
|
||||
public int PrepServiceId { get; set; }
|
||||
public int EstimatedMinutes { get; set; }
|
||||
|
||||
/// <summary>Which blast setup was selected when this sandblasting prep service was added.</summary>
|
||||
public int? BlastSetupId { get; set; }
|
||||
|
||||
public virtual QuoteItem QuoteItem { get; set; } = null!;
|
||||
public virtual PrepService PrepService { get; set; } = null!;
|
||||
public virtual CompanyBlastSetup? BlastSetup { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
public class QuotePhoto : BaseEntity
|
||||
{
|
||||
public int? QuoteId { get; set; } // null while in temp/pending state
|
||||
public string TempId { get; set; } = string.Empty; // GUID key used before quote is saved
|
||||
public string FilePath { get; set; } = string.Empty;
|
||||
public string FileName { get; set; } = string.Empty;
|
||||
public long FileSize { get; set; }
|
||||
public string ContentType { get; set; } = string.Empty;
|
||||
public string? Caption { get; set; }
|
||||
public bool IsAiAnalysisPhoto { get; set; } = true;
|
||||
public string? UploadedById { get; set; }
|
||||
|
||||
// Relationships
|
||||
public virtual Quote? Quote { get; set; }
|
||||
public virtual ApplicationUser? UploadedBy { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Join table for many-to-many relationship between Quote and PrepService
|
||||
/// </summary>
|
||||
public class QuotePrepService : BaseEntity
|
||||
{
|
||||
public int QuoteId { get; set; }
|
||||
public int PrepServiceId { get; set; }
|
||||
|
||||
// Navigation properties
|
||||
public virtual Quote Quote { get; set; } = null!;
|
||||
public virtual PrepService PrepService { get; set; } = null!;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Company-specific quote status lookup table.
|
||||
/// Replaces hardcoded QuoteStatus enum to enable workflow customization.
|
||||
/// </summary>
|
||||
public class QuoteStatusLookup : BaseEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Immutable status code used in code logic (e.g., "DRAFT", "APPROVED", "CONVERTED").
|
||||
/// Acts like the old enum name.
|
||||
/// </summary>
|
||||
public string StatusCode { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// User-customizable display name shown in UI (e.g., "Pending Approval", "Accepted by Customer").
|
||||
/// </summary>
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Display order for sorting statuses in dropdowns and reports.
|
||||
/// </summary>
|
||||
public int DisplayOrder { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Bootstrap color class for badge styling (primary, secondary, success, danger, warning, info, dark).
|
||||
/// Eliminates switch statement duplication for status colors.
|
||||
/// </summary>
|
||||
public string ColorClass { get; set; } = "secondary";
|
||||
|
||||
/// <summary>
|
||||
/// Optional Bootstrap icon class (e.g., "bi-file-earmark", "bi-check-circle").
|
||||
/// </summary>
|
||||
public string? IconClass { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this status is currently active and available for selection.
|
||||
/// </summary>
|
||||
public bool IsActive { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// System-defined statuses cannot be deleted.
|
||||
/// </summary>
|
||||
public bool IsSystemDefined { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// CRITICAL BUSINESS RULE: Exactly ONE status per company must be marked as the "approved" status.
|
||||
/// Only quotes with this status can be converted to jobs.
|
||||
/// Enforced by validation in CompanySettingsController.
|
||||
/// </summary>
|
||||
public bool IsApprovedStatus { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// CRITICAL BUSINESS RULE: Exactly ONE status per company must be marked as the "converted" status.
|
||||
/// This status is automatically set after a quote is successfully converted to a job.
|
||||
/// </summary>
|
||||
public bool IsConvertedStatus { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates the default draft status (enables expiration date logic, etc.).
|
||||
/// </summary>
|
||||
public bool IsDraftStatus { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// CRITICAL BUSINESS RULE: Marks the rejected/declined status.
|
||||
/// Used by the customer self-service approval portal to set the quote status when a customer declines.
|
||||
/// </summary>
|
||||
public bool IsRejectedStatus { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Optional description explaining when to use this status.
|
||||
/// </summary>
|
||||
public string? Description { get; set; }
|
||||
|
||||
// Navigation properties
|
||||
public virtual ICollection<Quote> Quotes { get; set; } = new List<Quote>();
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using PowderCoating.Core.Enums;
|
||||
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Records a refund issued to a customer against an invoice.
|
||||
/// Does not move actual money — the shop issues the refund manually.
|
||||
/// </summary>
|
||||
public class Refund : BaseEntity
|
||||
{
|
||||
public int InvoiceId { get; set; }
|
||||
public int? PaymentId { get; set; } // Specific payment being refunded (optional)
|
||||
|
||||
public decimal Amount { get; set; }
|
||||
public DateTime RefundDate { get; set; } = DateTime.UtcNow;
|
||||
public PaymentMethod RefundMethod { get; set; }
|
||||
public string Reason { get; set; } = string.Empty;
|
||||
public string? Reference { get; set; } // Check #, transaction ID, etc.
|
||||
public string? Notes { get; set; }
|
||||
|
||||
public RefundStatus Status { get; set; } = RefundStatus.Pending;
|
||||
public DateTime? IssuedDate { get; set; }
|
||||
public string? IssuedById { get; set; }
|
||||
|
||||
// For store-credit refunds: the CreditMemo created on their behalf
|
||||
public int? CreditMemoId { get; set; }
|
||||
|
||||
// Navigation
|
||||
public virtual Invoice Invoice { get; set; } = null!;
|
||||
public virtual Payment? Payment { get; set; }
|
||||
public virtual ApplicationUser? IssuedBy { get; set; }
|
||||
public virtual CreditMemo? CreditMemo { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Platform release notes / changelog entries published by SuperAdmins.
|
||||
/// Not a BaseEntity — platform-wide, not per-tenant.
|
||||
/// </summary>
|
||||
public class ReleaseNote
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>Semantic version string, e.g. "1.4.0"</summary>
|
||||
public string Version { get; set; } = string.Empty;
|
||||
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Markdown-formatted release notes body</summary>
|
||||
public string Body { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Display category tag, e.g. "Feature", "Improvement", "Fix", "Breaking"</summary>
|
||||
public string Tag { get; set; } = "Feature";
|
||||
|
||||
public bool IsPublished { get; set; } = false;
|
||||
|
||||
public DateTime ReleasedAt { get; set; } = DateTime.UtcNow;
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
|
||||
public string? CreatedByUserId { get; set; }
|
||||
public string? CreatedByUserName { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using PowderCoating.Core.Enums;
|
||||
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Tracks defect, warranty, or damage rework on a job.
|
||||
/// The original job owns the cost; a new linked job (ReworkJobId) handles the shop floor workflow.
|
||||
/// </summary>
|
||||
public class ReworkRecord : BaseEntity
|
||||
{
|
||||
// ── Links ─────────────────────────────────────────────────────────────────
|
||||
public int JobId { get; set; }
|
||||
public int? JobItemId { get; set; } // null = whole job; set = specific item
|
||||
|
||||
// Optional new job created to do the redo work on the shop floor
|
||||
public int? ReworkJobId { get; set; }
|
||||
|
||||
// ── Classification ────────────────────────────────────────────────────────
|
||||
public ReworkType ReworkType { get; set; }
|
||||
public ReworkReason Reason { get; set; }
|
||||
public string DefectDescription { get; set; } = string.Empty;
|
||||
|
||||
// ── Discovery ─────────────────────────────────────────────────────────────
|
||||
public ReworkDiscoveredBy DiscoveredBy { get; set; }
|
||||
public DateTime DiscoveredDate { get; set; } = DateTime.UtcNow;
|
||||
public string? ReportedByName { get; set; } // Customer name if external report
|
||||
|
||||
// ── Cost & Billing ────────────────────────────────────────────────────────
|
||||
public decimal EstimatedReworkCost { get; set; }
|
||||
public decimal ActualReworkCost { get; set; }
|
||||
public bool IsBillableToCustomer { get; set; }
|
||||
public string? BillingNotes { get; set; }
|
||||
|
||||
// ── Resolution ────────────────────────────────────────────────────────────
|
||||
public ReworkStatus Status { get; set; } = ReworkStatus.Open;
|
||||
public ReworkResolution? Resolution { get; set; }
|
||||
public DateTime? ResolvedDate { get; set; }
|
||||
public string? ResolutionNotes { get; set; }
|
||||
|
||||
// ── Navigation ────────────────────────────────────────────────────────────
|
||||
public virtual Job Job { get; set; } = null!;
|
||||
public virtual JobItem? JobItem { get; set; }
|
||||
public virtual Job? ReworkJob { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using PowderCoating.Core.Enums;
|
||||
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
public class ShopWorker : BaseEntity
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public ShopWorkerRole Role { get; set; } = ShopWorkerRole.GeneralLabor;
|
||||
public string? Phone { get; set; }
|
||||
public string? Email { get; set; }
|
||||
public bool IsActive { get; set; } = true;
|
||||
public string? Notes { get; set; }
|
||||
|
||||
// Relationships
|
||||
public virtual ICollection<Job> AssignedJobs { get; set; } = new List<Job>();
|
||||
public virtual ICollection<MaintenanceRecord> AssignedMaintenanceTasks { get; set; } = new List<MaintenanceRecord>();
|
||||
public virtual ICollection<JobTimeEntry> TimeEntries { get; set; } = new List<JobTimeEntry>();
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using PowderCoating.Core.Enums;
|
||||
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Optional per-role labor cost rate for job costing / profitability calculations.
|
||||
/// If no rate is set for a role, the company's StandardLaborRate is used as fallback.
|
||||
/// </summary>
|
||||
public class ShopWorkerRoleCost : BaseEntity
|
||||
{
|
||||
public ShopWorkerRole Role { get; set; }
|
||||
|
||||
/// <summary>Cost (pay rate) per hour for this role — used in job costing, NOT billing.</summary>
|
||||
public decimal HourlyRate { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Persists every incoming Stripe webhook event for auditing and debugging.
|
||||
/// Not a BaseEntity — platform-wide, no soft delete, no tenant filter.
|
||||
/// </summary>
|
||||
public class StripeWebhookEvent
|
||||
{
|
||||
public long Id { get; set; }
|
||||
|
||||
/// <summary>Stripe event ID (evt_...).</summary>
|
||||
public string EventId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Stripe event type string (e.g. customer.subscription.updated).</summary>
|
||||
public string EventType { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Linked company, if resolvable from the event payload.</summary>
|
||||
public int? CompanyId { get; set; }
|
||||
|
||||
/// <summary>Full raw JSON body of the event.</summary>
|
||||
public string RawJson { get; set; } = string.Empty;
|
||||
|
||||
public StripeWebhookEventStatus Status { get; set; } = StripeWebhookEventStatus.Received;
|
||||
|
||||
public string? ErrorMessage { get; set; }
|
||||
|
||||
public DateTime ReceivedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public DateTime? ProcessedAt { get; set; }
|
||||
}
|
||||
|
||||
public enum StripeWebhookEventStatus
|
||||
{
|
||||
Received = 0,
|
||||
Processed = 1,
|
||||
Failed = 2,
|
||||
Ignored = 3
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Stores plan limits and pricing in the database so SuperAdmins can edit without a code deploy.
|
||||
/// Global (not per-company). CompanyId is set to 0 / unused. No tenant query filter applied.
|
||||
/// The Plan integer is the identifier that links Company.SubscriptionPlan to a config row.
|
||||
/// </summary>
|
||||
public class SubscriptionPlanConfig : BaseEntity
|
||||
{
|
||||
public int Plan { get; set; }
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>-1 = unlimited</summary>
|
||||
public int MaxUsers { get; set; }
|
||||
/// <summary>-1 = unlimited</summary>
|
||||
public int MaxActiveJobs { get; set; }
|
||||
/// <summary>-1 = unlimited</summary>
|
||||
public int MaxCustomers { get; set; }
|
||||
/// <summary>-1 = unlimited. Counts active (non-terminal) quotes.</summary>
|
||||
public int MaxQuotes { get; set; } = -1;
|
||||
/// <summary>-1 = unlimited. Counts total non-deleted catalog items.</summary>
|
||||
public int MaxCatalogItems { get; set; } = -1;
|
||||
/// <summary>-1 = unlimited. Max job photos per job.</summary>
|
||||
public int MaxJobPhotos { get; set; } = -1;
|
||||
/// <summary>-1 = unlimited. Max general (non-AI) photos per quote.</summary>
|
||||
public int MaxQuotePhotos { get; set; } = -1;
|
||||
/// <summary>-1 = unlimited. 0 = AI photo quotes disabled for this plan. Monthly limit per company.</summary>
|
||||
public int MaxAiPhotoQuotesPerMonth { get; set; } = -1;
|
||||
|
||||
public decimal MonthlyPrice { get; set; }
|
||||
public decimal AnnualPrice { get; set; }
|
||||
|
||||
public string? StripePriceIdMonthly { get; set; }
|
||||
public string? StripePriceIdAnnual { get; set; }
|
||||
|
||||
/// <summary>When true, companies on this plan can connect Stripe and accept online invoice payments.</summary>
|
||||
public bool AllowOnlinePayments { get; set; } = false;
|
||||
|
||||
/// <summary>When true, companies on this plan can access accounting features: Chart of Accounts, Bills, Expenses, and Accounting Export.</summary>
|
||||
public bool AllowAccounting { get; set; } = false;
|
||||
|
||||
/// <summary>When true, companies on this plan can use AI Photo Quote analysis (subject to MaxAiPhotoQuotesPerMonth).</summary>
|
||||
public bool AllowAiPhotoQuotes { get; set; } = false;
|
||||
|
||||
/// <summary>When true, companies on this plan can use the AI Inventory Assist lookup feature.</summary>
|
||||
public bool AllowAiInventoryAssist { get; set; } = false;
|
||||
|
||||
public bool IsActive { get; set; } = true;
|
||||
public int SortOrder { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
using PowderCoating.Core.Enums;
|
||||
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
public class Vendor : BaseEntity
|
||||
{
|
||||
public string CompanyName { get; set; } = string.Empty;
|
||||
public string? ContactName { get; set; }
|
||||
public string? Email { get; set; }
|
||||
public string? Phone { get; set; }
|
||||
public string? Address { get; set; }
|
||||
public string? City { get; set; }
|
||||
public string? State { get; set; }
|
||||
public string? ZipCode { get; set; }
|
||||
public string? Country { get; set; } = "USA";
|
||||
|
||||
public string? Website { get; set; }
|
||||
|
||||
// Business Information
|
||||
public string? AccountNumber { get; set; }
|
||||
public string? TaxId { get; set; }
|
||||
public string? PaymentTerms { get; set; }
|
||||
public decimal? CreditLimit { get; set; }
|
||||
|
||||
public string? Notes { get; set; }
|
||||
public bool IsActive { get; set; } = true;
|
||||
public bool IsPreferred { get; set; } = false;
|
||||
|
||||
// Accounts Payable tracking
|
||||
/// <summary>Running AP balance: increases with new bills, decreases with payments.</summary>
|
||||
public decimal CurrentBalance { get; set; } = 0;
|
||||
/// <summary>Balance owed at go-live (for migrating existing vendors).</summary>
|
||||
public decimal OpeningBalance { get; set; } = 0;
|
||||
public DateTime? OpeningBalanceDate { get; set; }
|
||||
/// <summary>Default expense account pre-filled on new bill line items for this vendor.</summary>
|
||||
public int? DefaultExpenseAccountId { get; set; }
|
||||
|
||||
// Navigation
|
||||
public virtual ICollection<InventoryItem> InventoryItems { get; set; } = new List<InventoryItem>();
|
||||
public virtual ICollection<Bill> Bills { get; set; } = new List<Bill>();
|
||||
public virtual ICollection<BillPayment> BillPayments { get; set; } = new List<BillPayment>();
|
||||
public virtual ICollection<Expense> Expenses { get; set; } = new List<Expense>();
|
||||
public virtual Account? DefaultExpenseAccount { get; set; }
|
||||
}
|
||||
|
||||
public class InventoryTransaction : BaseEntity
|
||||
{
|
||||
public int InventoryItemId { get; set; }
|
||||
public InventoryTransactionType TransactionType { get; set; }
|
||||
public decimal Quantity { get; set; }
|
||||
public decimal UnitCost { get; set; }
|
||||
public decimal TotalCost { get; set; }
|
||||
|
||||
public DateTime TransactionDate { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public string? Reference { get; set; } // PO number, Job number, etc.
|
||||
public string? Notes { get; set; }
|
||||
|
||||
public decimal BalanceAfter { get; set; }
|
||||
|
||||
// Optional FK to the PO that generated this purchase transaction
|
||||
public int? PurchaseOrderId { get; set; }
|
||||
public virtual PurchaseOrder? PurchaseOrder { get; set; }
|
||||
|
||||
// Optional FK to the job this material was used on (set by QR scan / manual log)
|
||||
public int? JobId { get; set; }
|
||||
public virtual Job? Job { get; set; }
|
||||
|
||||
public virtual InventoryItem InventoryItem { get; set; } = null!;
|
||||
}
|
||||
|
||||
public class JobPhoto : BaseEntity
|
||||
{
|
||||
public int JobId { get; set; }
|
||||
public virtual Job Job { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Relative path from media folder (e.g., "1/job-photos/5/1.jpg")
|
||||
/// </summary>
|
||||
public string FilePath { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Original filename when uploaded
|
||||
/// </summary>
|
||||
public string FileName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// File size in bytes
|
||||
/// </summary>
|
||||
public long FileSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Content type (e.g., "image/jpeg")
|
||||
/// </summary>
|
||||
public string ContentType { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// User-provided caption/note for the photo
|
||||
/// </summary>
|
||||
public string? Caption { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Type of photo (Before, After, Progress, QualityCheck, Issue, Completed)
|
||||
/// </summary>
|
||||
public JobPhotoType PhotoType { get; set; } = JobPhotoType.Progress;
|
||||
|
||||
/// <summary>
|
||||
/// Display order for sorting photos
|
||||
/// </summary>
|
||||
public int DisplayOrder { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// User who uploaded the photo
|
||||
/// </summary>
|
||||
public string UploadedById { get; set; } = string.Empty;
|
||||
public virtual ApplicationUser? UploadedBy { get; set; }
|
||||
|
||||
public DateTime UploadedDate { get; set; } = DateTime.UtcNow;
|
||||
|
||||
/// <summary>
|
||||
/// Comma-separated tags for this photo (e.g., colors used, finish type)
|
||||
/// </summary>
|
||||
public string? Tags { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// True for photos copied from an AI quote analysis — excluded from subscription photo limits.
|
||||
/// </summary>
|
||||
public bool IsAiAnalysisPhoto { get; set; }
|
||||
}
|
||||
|
||||
public class JobNote : BaseEntity
|
||||
{
|
||||
public int JobId { get; set; }
|
||||
public string Note { get; set; } = string.Empty;
|
||||
public bool IsImportant { get; set; }
|
||||
public bool IsInternal { get; set; } = true; // Don't show to customer
|
||||
|
||||
public virtual Job Job { get; set; } = null!;
|
||||
}
|
||||
|
||||
public class CustomerNote : BaseEntity
|
||||
{
|
||||
public int CustomerId { get; set; }
|
||||
public string Note { get; set; } = string.Empty;
|
||||
public bool IsImportant { get; set; }
|
||||
|
||||
public virtual Customer Customer { get; set; } = null!;
|
||||
}
|
||||
|
||||
public class JobStatusHistory : BaseEntity
|
||||
{
|
||||
public int JobId { get; set; }
|
||||
|
||||
// Lookup foreign keys (replacing enums)
|
||||
public int FromStatusId { get; set; }
|
||||
public int ToStatusId { get; set; }
|
||||
|
||||
public DateTime ChangedDate { get; set; } = DateTime.UtcNow;
|
||||
public string? Notes { get; set; }
|
||||
|
||||
// Navigation properties
|
||||
public virtual Job Job { get; set; } = null!;
|
||||
public virtual JobStatusLookup FromStatus { get; set; } = null!;
|
||||
public virtual JobStatusLookup ToStatus { get; set; } = null!;
|
||||
}
|
||||
|
||||
public class PricingTier : BaseEntity
|
||||
{
|
||||
public string TierName { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
public decimal DiscountPercent { get; set; }
|
||||
public bool IsActive { get; set; } = true;
|
||||
|
||||
public virtual ICollection<Customer> Customers { get; set; } = new List<Customer>();
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Immutable audit record written once at registration. Never updated or deleted.
|
||||
/// </summary>
|
||||
public class TermsAcceptance
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string UserId { get; set; } = string.Empty;
|
||||
public int CompanyId { get; set; }
|
||||
public string TosVersion { get; set; } = string.Empty;
|
||||
public DateTime AcceptedAt { get; set; } = DateTime.UtcNow;
|
||||
public string? IpAddress { get; set; }
|
||||
public string? UserAgent { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
namespace PowderCoating.Core.Enums;
|
||||
|
||||
public enum AccountType
|
||||
{
|
||||
Asset = 1,
|
||||
Liability = 2,
|
||||
Equity = 3,
|
||||
Revenue = 4,
|
||||
CostOfGoods = 5,
|
||||
Expense = 6
|
||||
}
|
||||
|
||||
public enum AccountSubType
|
||||
{
|
||||
// Assets
|
||||
Checking = 1,
|
||||
Savings = 2,
|
||||
AccountsReceivable = 3,
|
||||
Inventory = 4,
|
||||
FixedAsset = 5,
|
||||
OtherCurrentAsset = 6,
|
||||
OtherAsset = 7,
|
||||
|
||||
// Liabilities
|
||||
AccountsPayable = 10,
|
||||
CreditCard = 11,
|
||||
OtherCurrentLiability = 12,
|
||||
LongTermLiability = 13,
|
||||
|
||||
// Equity
|
||||
OwnersEquity = 20,
|
||||
RetainedEarnings = 21,
|
||||
|
||||
// Revenue
|
||||
Sales = 30,
|
||||
ServiceRevenue = 31,
|
||||
OtherIncome = 32,
|
||||
|
||||
// Cost of Goods Sold
|
||||
CostOfGoodsSold = 40,
|
||||
|
||||
// Expenses
|
||||
Advertising = 50,
|
||||
SuppliesMaterials = 51,
|
||||
Equipment = 52,
|
||||
Insurance = 53,
|
||||
Payroll = 54,
|
||||
ProfessionalFees = 55,
|
||||
Rent = 56,
|
||||
Utilities = 57,
|
||||
Vehicle = 58,
|
||||
Travel = 59,
|
||||
Meals = 60,
|
||||
OfficeSupplies = 61,
|
||||
Depreciation = 62,
|
||||
BankCharges = 63,
|
||||
Other = 99
|
||||
}
|
||||
|
||||
public enum BillStatus
|
||||
{
|
||||
Draft = 0,
|
||||
Open = 1,
|
||||
PartiallyPaid = 2,
|
||||
Paid = 3,
|
||||
Voided = 4
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace PowderCoating.Core.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// Types of discounts that can be applied to quotes
|
||||
/// </summary>
|
||||
public enum DiscountType
|
||||
{
|
||||
/// <summary>
|
||||
/// No discount applied
|
||||
/// </summary>
|
||||
None = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Discount is a percentage of the subtotal (e.g., 10% off)
|
||||
/// </summary>
|
||||
Percentage = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Discount is a fixed dollar amount (e.g., $100 off)
|
||||
/// </summary>
|
||||
FixedAmount = 2
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
namespace PowderCoating.Core.Enums;
|
||||
|
||||
public enum JobStatus
|
||||
{
|
||||
Pending = 0,
|
||||
Quoted = 1,
|
||||
Approved = 2,
|
||||
InPreparation = 3,
|
||||
Sandblasting = 4,
|
||||
MaskingTaping = 5,
|
||||
Cleaning = 6,
|
||||
InOven = 7,
|
||||
Coating = 8,
|
||||
Curing = 9,
|
||||
QualityCheck = 10,
|
||||
Completed = 11,
|
||||
OnHold = 12,
|
||||
Cancelled = 13,
|
||||
ReadyForPickup = 14,
|
||||
Delivered = 15
|
||||
}
|
||||
|
||||
public enum JobPriority
|
||||
{
|
||||
Low = 0,
|
||||
Normal = 1,
|
||||
High = 2,
|
||||
Urgent = 3,
|
||||
Rush = 4
|
||||
}
|
||||
|
||||
public enum QuoteStatus
|
||||
{
|
||||
Draft = 0,
|
||||
Pending = 1,
|
||||
Sent = 2,
|
||||
Approved = 3,
|
||||
Rejected = 4,
|
||||
Expired = 5,
|
||||
Converted = 6
|
||||
}
|
||||
|
||||
public enum InventoryTransactionType
|
||||
{
|
||||
Purchase = 0,
|
||||
Sale = 1,
|
||||
Adjustment = 2,
|
||||
Transfer = 3,
|
||||
Return = 4,
|
||||
Waste = 5,
|
||||
Initial = 6,
|
||||
JobUsage = 7 // Powder consumed during job completion
|
||||
}
|
||||
|
||||
public enum MaintenanceStatus
|
||||
{
|
||||
Scheduled = 0,
|
||||
InProgress = 1,
|
||||
Completed = 2,
|
||||
Cancelled = 3,
|
||||
Overdue = 4
|
||||
}
|
||||
|
||||
public enum MaintenancePriority
|
||||
{
|
||||
Low = 0,
|
||||
Normal = 1,
|
||||
High = 2,
|
||||
Critical = 3
|
||||
}
|
||||
|
||||
public enum EquipmentStatus
|
||||
{
|
||||
Operational = 0,
|
||||
NeedsMaintenance = 1,
|
||||
UnderMaintenance = 2,
|
||||
OutOfService = 3,
|
||||
Retired = 4
|
||||
}
|
||||
|
||||
public enum ShopWorkerRole
|
||||
{
|
||||
GeneralLabor = 0,
|
||||
Sandblaster = 1,
|
||||
Coater = 2,
|
||||
Masker = 3,
|
||||
QualityControl = 4,
|
||||
OvenOperator = 5,
|
||||
Supervisor = 6,
|
||||
Maintenance = 7
|
||||
}
|
||||
|
||||
public enum JobPhotoType
|
||||
{
|
||||
Before = 0,
|
||||
Progress = 1,
|
||||
After = 2,
|
||||
QualityCheck = 3,
|
||||
Issue = 4,
|
||||
Completed = 5
|
||||
}
|
||||
|
||||
public enum MaintenanceRecurrenceFrequency
|
||||
{
|
||||
Daily = 1,
|
||||
Weekly = 2,
|
||||
BiWeekly = 3,
|
||||
Monthly = 4,
|
||||
Annually = 5,
|
||||
BiAnnually = 6,
|
||||
Quarterly = 7
|
||||
}
|
||||
|
||||
public enum ReworkType
|
||||
{
|
||||
InternalDefect = 0,
|
||||
CustomerWarranty = 1,
|
||||
CustomerDamage = 2
|
||||
}
|
||||
|
||||
public enum ReworkReason
|
||||
{
|
||||
AdhesionFailure = 0,
|
||||
Contamination = 1,
|
||||
ColorMismatch = 2,
|
||||
RunsSags = 3,
|
||||
SurfacePrepFailure = 4,
|
||||
OvenIssue = 5,
|
||||
InsufficientCoverage = 6,
|
||||
HandlingDamage = 7,
|
||||
Other = 8
|
||||
}
|
||||
|
||||
public enum ReworkDiscoveredBy
|
||||
{
|
||||
Internal = 0,
|
||||
Customer = 1
|
||||
}
|
||||
|
||||
public enum ReworkStatus
|
||||
{
|
||||
Open = 0,
|
||||
InProgress = 1,
|
||||
Resolved = 2,
|
||||
WrittenOff = 3,
|
||||
Disputed = 4
|
||||
}
|
||||
|
||||
public enum ReworkResolution
|
||||
{
|
||||
RecoatedNoCharge = 0,
|
||||
RecoatedBilled = 1,
|
||||
CustomerCredited = 2,
|
||||
WrittenOff = 3,
|
||||
NoActionRequired = 4
|
||||
}
|
||||
|
||||
public enum BugReportStatus
|
||||
{
|
||||
New = 0,
|
||||
InProgress = 1,
|
||||
Completed = 2,
|
||||
Cancelled = 3
|
||||
}
|
||||
|
||||
public enum BugReportPriority
|
||||
{
|
||||
Low = 0,
|
||||
Normal = 1,
|
||||
High = 2,
|
||||
Critical = 3
|
||||
}
|
||||
|
||||
public enum OvenBatchStatus
|
||||
{
|
||||
Planned = 0,
|
||||
Loading = 1,
|
||||
InProgress = 2,
|
||||
Completed = 3,
|
||||
Cancelled = 4
|
||||
}
|
||||
|
||||
public enum OvenBatchItemStatus
|
||||
{
|
||||
Pending = 0,
|
||||
InOven = 1,
|
||||
Completed = 2,
|
||||
Removed = 3
|
||||
}
|
||||
|
||||
public enum PurchaseOrderStatus
|
||||
{
|
||||
Draft = 0,
|
||||
Submitted = 1,
|
||||
PartiallyReceived = 2,
|
||||
Received = 3,
|
||||
Cancelled = 4
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
namespace PowderCoating.Core.Enums;
|
||||
|
||||
public enum RefundStatus
|
||||
{
|
||||
Pending = 0, // Recorded — shop has not yet physically issued the refund
|
||||
Issued = 1, // Refund has been sent to the customer
|
||||
Cancelled = 2
|
||||
}
|
||||
|
||||
public enum CreditMemoStatus
|
||||
{
|
||||
Active = 0,
|
||||
PartiallyApplied = 1,
|
||||
FullyApplied = 2,
|
||||
Voided = 3
|
||||
}
|
||||
|
||||
public enum InvoiceStatus
|
||||
{
|
||||
Draft = 0,
|
||||
Sent = 1,
|
||||
PartiallyPaid = 2,
|
||||
Paid = 3,
|
||||
Overdue = 4,
|
||||
Voided = 5,
|
||||
WrittenOff = 6
|
||||
}
|
||||
|
||||
public enum PaymentMethod
|
||||
{
|
||||
Cash = 0,
|
||||
Check = 1,
|
||||
CreditDebitCard = 2,
|
||||
BankTransferACH = 3,
|
||||
DigitalPayment = 4,
|
||||
StoreCredit = 5 // Refund issued as store credit (creates a CreditMemo)
|
||||
}
|
||||
|
||||
public enum GiftCertificateStatus
|
||||
{
|
||||
Active = 0,
|
||||
PartiallyRedeemed = 1,
|
||||
FullyRedeemed = 2,
|
||||
Expired = 3,
|
||||
Voided = 4
|
||||
}
|
||||
|
||||
public enum OnlinePaymentStatus
|
||||
{
|
||||
NotApplicable = 0, // Online payments not enabled for this company
|
||||
Pending = 1, // Link generated, not yet paid
|
||||
PartiallyPaid = 2, // Customer has made one or more partial payments
|
||||
Paid = 3, // Fully paid via online payment
|
||||
Refunded = 4 // Online payment was refunded via Stripe
|
||||
}
|
||||
|
||||
public enum OnlinePaymentSurchargeType
|
||||
{
|
||||
None = 0,
|
||||
Percent = 1, // e.g. 2.9% of transaction
|
||||
Flat = 2 // e.g. $1.50 flat fee
|
||||
}
|
||||
|
||||
public enum StripeConnectStatus
|
||||
{
|
||||
NotConnected = 0,
|
||||
Pending = 1, // OAuth started, not yet completed
|
||||
Active = 2, // Connected and ready to accept payments
|
||||
Disabled = 3 // Manually disabled or deauthorized by the company
|
||||
}
|
||||
|
||||
public enum GiftCertificateIssuedReason
|
||||
{
|
||||
Sold = 0,
|
||||
Prize = 1,
|
||||
Promotional = 2,
|
||||
Goodwill = 3,
|
||||
Other = 4
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace PowderCoating.Core.Enums;
|
||||
|
||||
public enum NotificationChannel { Email = 0, Sms = 1 }
|
||||
public enum NotificationStatus { Sent = 0, Failed = 1, Skipped = 2 }
|
||||
public enum NotificationType
|
||||
{
|
||||
QuoteSent = 0,
|
||||
QuoteApproved = 1,
|
||||
JobStatusChanged = 2,
|
||||
JobReadyForPickup = 3,
|
||||
JobCompleted = 4,
|
||||
SmsConsentConfirmation = 5,
|
||||
InvoiceSent = 6,
|
||||
PaymentReceived = 7,
|
||||
QuoteDeclinedByCustomer = 8,
|
||||
PaymentReminder = 9,
|
||||
SubscriptionExpiryReminder = 10,
|
||||
SubscriptionExpired = 11,
|
||||
SmsInboundStop = 12,
|
||||
SmsInboundHelp = 13
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace PowderCoating.Core.Enums;
|
||||
|
||||
public enum PricingMode
|
||||
{
|
||||
/// <summary>Markup % applied to material costs only. Labor and equipment pass through at cost.</summary>
|
||||
MarkupOnMaterial = 0,
|
||||
|
||||
/// <summary>Target margin % applied to total item cost (material + labor + equipment).</summary>
|
||||
MarginOnTotalCost = 1
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
namespace PowderCoating.Core.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// Broad capability tier selected during onboarding. Sets default equipment profile values
|
||||
/// so new shops get reasonable estimates without completing full calibration.
|
||||
/// </summary>
|
||||
public enum ShopCapabilityTier
|
||||
{
|
||||
/// <summary>Home garage coater, small compressor, siphon cabinet.</summary>
|
||||
Garage = 0,
|
||||
/// <summary>1-5 person shop with moderate equipment.</summary>
|
||||
Small = 1,
|
||||
/// <summary>Established shop, pressure pot, 5-10 people.</summary>
|
||||
Medium = 2,
|
||||
/// <summary>High-volume operation, large pressure pots, 10+ people.</summary>
|
||||
Large = 3
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sandblasting equipment configuration. Pressure pot delivers ~2x the media
|
||||
/// velocity of a siphon-fed setup, significantly affecting sqft/hr throughput.
|
||||
/// </summary>
|
||||
public enum BlastSetupType
|
||||
{
|
||||
SiphonCabinet = 0,
|
||||
SiphonPot = 1,
|
||||
PressurePot = 2,
|
||||
WetBlasting = 3
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Primary coating/substrate being removed. Affects how many passes are needed
|
||||
/// and therefore the effective blast rate per sqft.
|
||||
/// </summary>
|
||||
public enum BlastSubstrateType
|
||||
{
|
||||
Paint = 0,
|
||||
PowderCoat = 1,
|
||||
RustAndScale = 2,
|
||||
Mixed = 3
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Powder coating gun technology. Affects application speed and first-pass
|
||||
/// transfer efficiency, especially on complex geometry.
|
||||
/// </summary>
|
||||
public enum CoatingGunType
|
||||
{
|
||||
Corona = 0,
|
||||
Tribo = 1,
|
||||
Both = 2
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace PowderCoating.Core.Enums;
|
||||
|
||||
public enum SubscriptionStatus
|
||||
{
|
||||
Active = 0,
|
||||
GracePeriod = 1,
|
||||
Expired = 2,
|
||||
Canceled = 3,
|
||||
Inactive = 4
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using System.Linq.Expressions;
|
||||
using PowderCoating.Core.Entities;
|
||||
|
||||
namespace PowderCoating.Core.Interfaces;
|
||||
|
||||
public interface IRepository<T> where T : BaseEntity
|
||||
{
|
||||
Task<T?> GetByIdAsync(int id, bool ignoreQueryFilters = false, params Expression<Func<T, object>>[] includes);
|
||||
Task<IEnumerable<T>> GetAllAsync(bool ignoreQueryFilters = false, params Expression<Func<T, object>>[] includes);
|
||||
Task<IEnumerable<T>> FindAsync(Expression<Func<T, bool>> predicate, bool ignoreQueryFilters = false, params Expression<Func<T, object>>[] includes);
|
||||
Task<T?> FirstOrDefaultAsync(Expression<Func<T, bool>> predicate, bool ignoreQueryFilters = false, params Expression<Func<T, object>>[] includes);
|
||||
Task<bool> AnyAsync(Expression<Func<T, bool>> predicate, bool ignoreQueryFilters = false);
|
||||
Task<int> CountAsync(Expression<Func<T, bool>>? predicate = null, bool ignoreQueryFilters = false);
|
||||
|
||||
Task<T> AddAsync(T entity);
|
||||
Task<IEnumerable<T>> AddRangeAsync(IEnumerable<T> entities);
|
||||
|
||||
Task UpdateAsync(T entity);
|
||||
Task UpdateRangeAsync(IEnumerable<T> entities);
|
||||
|
||||
Task DeleteAsync(T entity);
|
||||
Task DeleteAsync(int id);
|
||||
Task DeleteRangeAsync(IEnumerable<T> entities);
|
||||
|
||||
// Soft delete
|
||||
Task SoftDeleteAsync(T entity);
|
||||
Task SoftDeleteAsync(int id);
|
||||
|
||||
// Pagination
|
||||
Task<(IEnumerable<T> Items, int TotalCount)> GetPagedAsync(
|
||||
int pageNumber,
|
||||
int pageSize,
|
||||
Expression<Func<T, bool>>? filter = null,
|
||||
Func<IQueryable<T>, IOrderedQueryable<T>>? orderBy = null,
|
||||
params Expression<Func<T, object>>[] includes);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using PowderCoating.Core.Entities;
|
||||
using PowderCoating.Core.Enums;
|
||||
|
||||
namespace PowderCoating.Core.Interfaces;
|
||||
|
||||
public interface ISubscriptionService
|
||||
{
|
||||
Task<bool> CanAddUserAsync(int companyId);
|
||||
Task<bool> CanAddJobAsync(int companyId);
|
||||
Task<bool> CanAddCustomerAsync(int companyId);
|
||||
Task<bool> CanAddQuoteAsync(int companyId);
|
||||
Task<bool> CanAddCatalogItemAsync(int companyId);
|
||||
Task<bool> CanAddJobPhotoAsync(int companyId, int jobId);
|
||||
Task<bool> CanAddQuotePhotoAsync(int companyId, int quoteId);
|
||||
Task<(int Used, int Max)> GetUserCountAsync(int companyId);
|
||||
Task<(int Used, int Max)> GetJobCountAsync(int companyId);
|
||||
Task<(int Used, int Max)> GetCustomerCountAsync(int companyId);
|
||||
Task<(int Used, int Max)> GetQuoteCountAsync(int companyId);
|
||||
Task<(int Used, int Max)> GetCatalogItemCountAsync(int companyId);
|
||||
Task<(int Used, int Max)> GetJobPhotoCountAsync(int companyId, int jobId);
|
||||
Task<(int Used, int Max)> GetQuotePhotoCountAsync(int companyId, int quoteId);
|
||||
Task<SubscriptionStatus> GetStatusAsync(int companyId);
|
||||
|
||||
// AI feature gating
|
||||
/// <summary>Returns true if the AI Inventory Assist lookup is enabled for this company.</summary>
|
||||
Task<bool> IsAiInventoryAssistEnabledAsync(int companyId);
|
||||
/// <summary>Returns true if the AI Photo Quote feature is enabled for this company (flag + quota check).</summary>
|
||||
Task<bool> CanUseAiPhotoQuoteAsync(int companyId);
|
||||
/// <summary>Returns (used this month, monthly max). Max = -1 means unlimited.</summary>
|
||||
Task<(int Used, int Max)> GetAiPhotoQuoteUsageAsync(int companyId);
|
||||
|
||||
/// <summary>
|
||||
/// Returns days until expiry (negative = days past expiry). Returns null if no end date set.
|
||||
/// </summary>
|
||||
int? DaysUntilExpiry(Company company);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using PowderCoating.Core.Entities;
|
||||
|
||||
namespace PowderCoating.Core.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Provides context about the current tenant (company) in the multi-tenant system
|
||||
/// </summary>
|
||||
public interface ITenantContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the current company ID from the authenticated user's claims
|
||||
/// </summary>
|
||||
/// <returns>The company ID if user is authenticated, null otherwise</returns>
|
||||
int? GetCurrentCompanyId();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the full company entity for the current user
|
||||
/// </summary>
|
||||
/// <returns>The company entity if user is authenticated, null otherwise</returns>
|
||||
Task<Company?> GetCurrentCompanyAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the current user is a SuperAdmin (platform administrator)
|
||||
/// </summary>
|
||||
/// <returns>True if user is SuperAdmin, false otherwise</returns>
|
||||
bool IsSuperAdmin();
|
||||
|
||||
/// <summary>
|
||||
/// Returns true only for SuperAdmins tied to the platform demo company (ID 1).
|
||||
/// Platform admins have unrestricted cross-company data access.
|
||||
/// Company SuperAdmins (CompanyId != 1) are scoped to their own company's data.
|
||||
/// </summary>
|
||||
bool IsPlatformAdmin();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the metric system preference for the current company
|
||||
/// </summary>
|
||||
/// <returns>True if metric system is enabled, false for imperial system</returns>
|
||||
Task<bool> UseMetricSystemAsync();
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
using PowderCoating.Core.Entities;
|
||||
|
||||
namespace PowderCoating.Core.Interfaces;
|
||||
|
||||
public interface IUnitOfWork : IDisposable
|
||||
{
|
||||
// Multi-tenancy
|
||||
IRepository<Company> Companies { get; }
|
||||
IRepository<CompanyOperatingCosts> CompanyOperatingCosts { get; }
|
||||
IRepository<CompanyPreferences> CompanyPreferences { get; }
|
||||
|
||||
// AI Predictions
|
||||
IRepository<AiItemPrediction> AiItemPredictions { get; }
|
||||
|
||||
// Powder Insights
|
||||
IRepository<PowderUsageLog> PowderUsageLogs { get; }
|
||||
|
||||
// Core entities
|
||||
IRepository<Customer> Customers { get; }
|
||||
IRepository<Job> Jobs { get; }
|
||||
IRepository<JobDailyPriority> JobDailyPriorities { get; }
|
||||
IRepository<JobItem> JobItems { get; }
|
||||
IRepository<JobItemCoat> JobItemCoats { get; }
|
||||
IRepository<JobChangeHistory> JobChangeHistories { get; }
|
||||
IRepository<Quote> Quotes { get; }
|
||||
IRepository<QuotePhoto> QuotePhotos { get; }
|
||||
IRepository<QuoteItem> QuoteItems { get; }
|
||||
IRepository<QuoteItemCoat> QuoteItemCoats { get; }
|
||||
IRepository<QuoteItemPrepService> QuoteItemPrepServices { get; }
|
||||
IRepository<QuoteChangeHistory> QuoteChangeHistories { get; }
|
||||
IRepository<InventoryItem> InventoryItems { get; }
|
||||
IRepository<InventoryTransaction> InventoryTransactions { get; }
|
||||
IRepository<Equipment> Equipment { get; }
|
||||
IRepository<OvenCost> OvenCosts { get; }
|
||||
IRepository<CompanyBlastSetup> BlastSetups { get; }
|
||||
IRepository<MaintenanceRecord> MaintenanceRecords { get; }
|
||||
IRepository<Vendor> Vendors { get; }
|
||||
IRepository<JobPhoto> JobPhotos { get; }
|
||||
IRepository<JobNote> JobNotes { get; }
|
||||
IRepository<CustomerNote> CustomerNotes { get; }
|
||||
IRepository<JobStatusHistory> JobStatusHistory { get; }
|
||||
IRepository<PricingTier> PricingTiers { get; }
|
||||
|
||||
// Lookup tables (replacing enums)
|
||||
IRepository<JobStatusLookup> JobStatusLookups { get; }
|
||||
IRepository<JobPriorityLookup> JobPriorityLookups { get; }
|
||||
IRepository<QuoteStatusLookup> QuoteStatusLookups { get; }
|
||||
IRepository<InventoryCategoryLookup> InventoryCategoryLookups { get; }
|
||||
IRepository<AppointmentStatusLookup> AppointmentStatusLookups { get; }
|
||||
IRepository<AppointmentTypeLookup> AppointmentTypeLookups { get; }
|
||||
IRepository<PrepService> PrepServices { get; }
|
||||
IRepository<ShopWorker> ShopWorkers { get; }
|
||||
IRepository<ShopWorkerRoleCost> ShopWorkerRoleCosts { get; }
|
||||
IRepository<ReworkRecord> ReworkRecords { get; }
|
||||
IRepository<Refund> Refunds { get; }
|
||||
IRepository<CreditMemo> CreditMemos { get; }
|
||||
IRepository<CreditMemoApplication> CreditMemoApplications { get; }
|
||||
IRepository<JobTimeEntry> JobTimeEntries { get; }
|
||||
|
||||
// Appointments
|
||||
IRepository<Appointment> Appointments { get; }
|
||||
|
||||
// Product Catalog
|
||||
IRepository<CatalogCategory> CatalogCategories { get; }
|
||||
IRepository<CatalogItem> CatalogItems { get; }
|
||||
|
||||
// Oven Scheduling
|
||||
IRepository<OvenBatch> OvenBatches { get; }
|
||||
IRepository<OvenBatchItem> OvenBatchItems { get; }
|
||||
|
||||
// Invoices, Payments & Deposits
|
||||
IRepository<Invoice> Invoices { get; }
|
||||
IRepository<InvoiceItem> InvoiceItems { get; }
|
||||
IRepository<Payment> Payments { get; }
|
||||
IRepository<Deposit> Deposits { get; }
|
||||
|
||||
// Purchase Orders
|
||||
IRepository<PurchaseOrder> PurchaseOrders { get; }
|
||||
IRepository<PurchaseOrderItem> PurchaseOrderItems { get; }
|
||||
|
||||
// Expense Tracking / Accounts Payable
|
||||
IRepository<Account> Accounts { get; }
|
||||
IRepository<Bill> Bills { get; }
|
||||
IRepository<BillLineItem> BillLineItems { get; }
|
||||
IRepository<BillPayment> BillPayments { get; }
|
||||
IRepository<Expense> Expenses { get; }
|
||||
|
||||
// Notifications
|
||||
IRepository<NotificationLog> NotificationLogs { get; }
|
||||
IRepository<NotificationTemplate> NotificationTemplates { get; }
|
||||
|
||||
// Subscription
|
||||
IRepository<SubscriptionPlanConfig> SubscriptionPlanConfigs { get; }
|
||||
|
||||
// Job Templates
|
||||
IRepository<JobTemplate> JobTemplates { get; }
|
||||
IRepository<JobTemplateItem> JobTemplateItems { get; }
|
||||
IRepository<JobTemplateItemCoat> JobTemplateItemCoats { get; }
|
||||
IRepository<JobTemplateItemPrepService> JobTemplateItemPrepServices { get; }
|
||||
|
||||
// Bug Reports
|
||||
IRepository<BugReport> BugReports { get; }
|
||||
|
||||
// Contact Us
|
||||
IRepository<ContactSubmission> ContactSubmissions { get; }
|
||||
|
||||
// AI lookup: per-manufacturer URL patterns
|
||||
IRepository<ManufacturerLookupPattern> ManufacturerLookupPatterns { get; }
|
||||
|
||||
// Gift Certificates
|
||||
IRepository<GiftCertificate> GiftCertificates { get; }
|
||||
IRepository<GiftCertificateRedemption> GiftCertificateRedemptions { get; }
|
||||
|
||||
Task<int> SaveChangesAsync();
|
||||
Task<int> CompleteAsync(); // Alias for SaveChangesAsync
|
||||
|
||||
/// <summary>
|
||||
/// Executes <paramref name="operation"/> inside a database transaction using EF Core's
|
||||
/// execution strategy, enabling compatibility with SqlServerRetryingExecutionStrategy.
|
||||
/// Commits on success and rolls back on any exception (which is re-thrown).
|
||||
/// </summary>
|
||||
Task ExecuteInTransactionAsync(Func<Task> operation);
|
||||
|
||||
/// <summary>
|
||||
/// Same as <see cref="ExecuteInTransactionAsync(Func{Task})"/> but returns a value.
|
||||
/// </summary>
|
||||
Task<T> ExecuteInTransactionAsync<T>(Func<Task<T>> operation);
|
||||
|
||||
/// <summary>
|
||||
/// Detaches all tracked entities from the change tracker.
|
||||
/// Use after a failed save to prevent contaminating subsequent operations.
|
||||
/// </summary>
|
||||
void ClearChangeTracker();
|
||||
|
||||
// Kept for backwards-compatibility — prefer ExecuteInTransactionAsync for new code.
|
||||
Task BeginTransactionAsync();
|
||||
Task CommitTransactionAsync();
|
||||
Task RollbackTransactionAsync();
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Identity.Stores" Version="8.0.11" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user