diff --git a/src/JoinRpg.DataModel/Claim.cs b/src/JoinRpg.DataModel/Claim.cs index 0489a49cf..81d78caa2 100644 --- a/src/JoinRpg.DataModel/Claim.cs +++ b/src/JoinRpg.DataModel/Claim.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using JoinRpg.DataModel.Finances; using JoinRpg.Helpers; @@ -7,7 +8,7 @@ namespace JoinRpg.DataModel; // ReSharper disable once ClassWithVirtualMembersNeverInherited.Global used by LINQ -public class Claim : IProjectEntity, ILinkable, IFieldContainter +public class Claim : IProjectEntity, ILinkable, IFieldContainter, IValidatableObject { public int ClaimId { get; set; } public int CharacterId { get; set; } @@ -81,11 +82,32 @@ public class Claim : IProjectEntity, ILinkable, IFieldContainter #region Finance + /// + /// The date when necessary amout to stop increasing of the total price has been paid. + /// + public DateTime? FixedPaymentDate { get; set; } + + /// + /// Discounted price to be paid for this claim. + /// When set, total price will not be automatically recalculated depending on the current date. + /// Mutually exclusive with . + /// + [Range(0, int.MaxValue)] + public int? DiscountedPrice { get; set; } + + /// + /// Discount percent for this claim. Do not affect the total price recalculation depending on the current date. + /// Mutually exclusive with . + /// + [Range(0, 100)] + public int? DiscountPercent { get; set; } + /// /// Fee to pay by player, manually set by master. /// If null (default), actual fee will be automatically selected /// from the project's list of payments. /// + [Obsolete("Use the DiscountedPrice or DiscountPercent instead")] public int? CurrentFee { get; set; } /// @@ -104,6 +126,7 @@ public IEnumerable ApprovedFinanceOperations /// public virtual ICollection RecurrentPayments { get; set; } + [Obsolete("Set the discount directly")] public bool PreferentialFeeUser { get; set; } /// @@ -139,4 +162,14 @@ public IEnumerable ApprovedFinanceOperations #endregion + /// + public IEnumerable Validate(ValidationContext validationContext) + { + if (DiscountedPrice.HasValue && DiscountPercent.HasValue) + { + yield return new ValidationResult( + "Only one type of discount could be set", + [nameof(DiscountedPrice), nameof(DiscountPercent)]); + } + } } diff --git a/src/JoinRpg.DataModel/Finances/PriceValue.cs b/src/JoinRpg.DataModel/Finances/PriceValue.cs new file mode 100644 index 000000000..1bfd461be --- /dev/null +++ b/src/JoinRpg.DataModel/Finances/PriceValue.cs @@ -0,0 +1,33 @@ +using System.ComponentModel.DataAnnotations; +using JoinRpg.PrimitiveTypes.ProjectMetadata; + +namespace JoinRpg.DataModel.Finances; + +public class PriceValue +{ + /// + /// Database Id of a project. + /// + public int ProjectId { get; set; } + + /// + /// Database Id of a date-time column. + /// + public int ProjectFeeSettingId { get; set; } + + /// + /// Database Id of a field. + /// + public int FieldId { get; set; } + + /// + /// Database Id of a field value (only for and ). + /// + public int? FieldValueId { get; set; } + + /// + /// Price value (only positive values are allowed). + /// + [Range(0, int.MaxValue, ErrorMessage = "Fee should be positive.")] + public int Price { get; set; } +} diff --git a/src/JoinRpg.DataModel/Finances/ProjectFeeSetting.cs b/src/JoinRpg.DataModel/Finances/ProjectFeeSetting.cs index 804e6d43e..422ca074f 100644 --- a/src/JoinRpg.DataModel/Finances/ProjectFeeSetting.cs +++ b/src/JoinRpg.DataModel/Finances/ProjectFeeSetting.cs @@ -5,11 +5,18 @@ namespace JoinRpg.DataModel; public class ProjectFeeSetting { public int ProjectFeeSettingId { get; set; } + public int ProjectId { get; set; } + public virtual Project Project { get; set; } + + [Obsolete("Date-dependent prices are now configured using PriveValue")] [Range(0, int.MaxValue, ErrorMessage = "Fee should be positive.")] public int Fee { get; set; } + + [Obsolete("Date-dependent prices are now configured using PriveValue")] [Range(0, int.MaxValue, ErrorMessage = "Fee should be positive.")] public int? PreferentialFee { get; set; } + public DateTime StartDate { get; set; } } diff --git a/src/JoinRpg.DataModel/Finances/ReceiptItemType.cs b/src/JoinRpg.DataModel/Finances/ReceiptItemType.cs new file mode 100644 index 000000000..f87dfe822 --- /dev/null +++ b/src/JoinRpg.DataModel/Finances/ReceiptItemType.cs @@ -0,0 +1,22 @@ +namespace JoinRpg.DataModel.Finances; + +/// +/// Describes how a field with price should be treated when issuing a receipt. +/// +public enum ReceiptItemType +{ + /// + /// Field should not have its own receipt position. + /// + IncludeIntoPrimary, + + /// + /// Field represents a certain commodity. + /// + Commodity, + + /// + /// Field represents a certain service. + /// + Service, +} diff --git a/src/JoinRpg.DataModel/ProjectField.cs b/src/JoinRpg.DataModel/ProjectField.cs index ad0fdca06..1f0135943 100644 --- a/src/JoinRpg.DataModel/ProjectField.cs +++ b/src/JoinRpg.DataModel/ProjectField.cs @@ -1,5 +1,6 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; +using JoinRpg.DataModel.Finances; using JoinRpg.Helpers; using JoinRpg.PrimitiveTypes.ProjectMetadata; @@ -44,16 +45,56 @@ public class ProjectField : IProjectEntity, IDeletableSubEntity, IValidatableObj public bool ShowOnUnApprovedClaims { get; set; } /// - /// Price associated with current field. - /// Will be used in payment calculations. - /// Value usage differs on FieldType. Value will be: - /// - Number: multiplied with entered number - /// - CheckBox: used if checkbox was checked - /// - Dropdown, Multiselect: ignored, see values for prices - /// - String, Text, Header: ignored + /// When true, this field may have assigned prices. /// + /// + /// Has effect only when field has certain : + ///
    + ///
  • + ///
  • + ///
  • + ///
  • + ///
+ ///
+ public bool Payable { get; set; } + + /// + /// Base price associated with this field. + /// + /// + /// Actual price depends on current date via objects. + /// public int Price { get; set; } + /// + /// Defines how this field should be interpreted when preparing a receipt. + /// + public ReceiptItemType ReceiptItemType { get; set; } + + /// + /// The name used to display in receipt. When not specified, the is used. + /// + /// + /// When is or + /// this value has to be specified independently in each . + /// + [StringLength(64)] + public string? ReceiptName { get; set; } + + /// + /// When true, a discount can be applied to price of this field. + /// + /// + /// Whereas the relative discount set in will only affect the total sum while calculation, + /// the will exclude discountable field from total calculation. + /// + public bool Discountable { get; set; } + + /// + /// Collection of related price values. + /// + public ICollection? PriceValues { get; set; } + bool IDeletableSubEntity.CanBePermanentlyDeleted => !WasEverUsed; public virtual ICollection DropdownValues { get; set; } = diff --git a/src/JoinRpg.DataModel/ProjectFieldDropdownValue.cs b/src/JoinRpg.DataModel/ProjectFieldDropdownValue.cs index c35266fe0..c2c4096e5 100644 --- a/src/JoinRpg.DataModel/ProjectFieldDropdownValue.cs +++ b/src/JoinRpg.DataModel/ProjectFieldDropdownValue.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using JoinRpg.DataModel.Finances; using JoinRpg.Helpers; namespace JoinRpg.DataModel; @@ -32,17 +33,35 @@ public class ProjectFieldDropdownValue : IDeletableSubEntity, IProjectEntity, IV public MarkdownString MasterDescription { get; set; } + #region Finance + /// - /// Price associated with this value. + /// Base price associated with this field value. /// + /// + /// Actual price depends on current date via objects. + /// public int Price { get; set; } + /// + /// The name used to display in receipt. + /// When not specified, the property value will be taken but truncated to 64 characters. + /// + [StringLength(64)] + public string? ReceiptName { get; set; } + + /// + /// Collection of related price values. + /// + public ICollection? PriceValues { get; set; } + + #endregion + /// /// External value for external IT systems /// public string? ProgrammaticValue { get; set; } - public virtual CharacterGroup? CharacterGroup { get; set; } public virtual int? CharacterGroupId { get; set; } diff --git a/src/JoinRpg.DataModel/Projects/Project.cs b/src/JoinRpg.DataModel/Projects/Project.cs index 7478ae1a3..76742ac15 100644 --- a/src/JoinRpg.DataModel/Projects/Project.cs +++ b/src/JoinRpg.DataModel/Projects/Project.cs @@ -30,17 +30,24 @@ public class Project : IProjectEntity public virtual ICollection Claims { get; set; } - public virtual ICollection FinanceOperations { get; set; } - public virtual ProjectDetails Details { get; set; } public virtual ICollection PlotFolders { get; set; } Project IProjectEntity.Project => this; + #region Finance + public virtual ICollection ProjectFeeSettings { get; set; } + public virtual ICollection PaymentTypes { get; set; } + public virtual ICollection MoneyTransfers { get; set; } + + public virtual ICollection FinanceOperations { get; set; } + + #endregion + public DateTime CharacterTreeModifiedAt { get; set; } #region helper properties @@ -53,7 +60,5 @@ public class Project : IProjectEntity public virtual ICollection GameReport2DTemplates { get; set; } - public virtual ICollection MoneyTransfers { get; set; } - public virtual HashSet KogdaIgraGames { get; set; } = null!; } diff --git a/src/JoinRpg.DataModel/Projects/ProjectDetails.cs b/src/JoinRpg.DataModel/Projects/ProjectDetails.cs index 3669ea4c7..b50b1d45a 100644 --- a/src/JoinRpg.DataModel/Projects/ProjectDetails.cs +++ b/src/JoinRpg.DataModel/Projects/ProjectDetails.cs @@ -1,5 +1,6 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; +using JoinRpg.DataModel.Finances; using JoinRpg.PrimitiveTypes.ProjectMetadata; namespace JoinRpg.DataModel; @@ -13,10 +14,61 @@ public class ProjectDetails : IValidatableObject public bool EnableManyCharacters { get; set; } public bool PublishPlot { get; set; } + #region Finance + public bool FinanceWarnOnOverPayment { get; set; } = true; + + /// + /// The minimal required payment to be made to stop increasing the total price depending on date. + /// + /// + /// This property is not mutually exclusive with . + /// When both defined, price will be fixed when the first condition has met. + /// + [Range(0, int.MaxValue)] + public int? MinPaymentToFixPrice { get; set; } + + /// + /// The minimal required payment in percent from total to be made to stop increasing the total price depending on date. + /// + /// + /// This property is not mutually exclusive with . + /// When both defined, price will be fixed when the first condition has met. + /// + [Range(0, 100)] + public int? MinPaidPercentToFixPrice { get; set; } + + /// + /// When true, a discount could be set for a claim payment. + /// + public bool EnableDiscounts { get; set; } + + /// + /// Describes the conditions a user should meet to get a discount. + /// + public MarkdownString DiscountConditions { get; set; } = new(); + + /// + /// Defines how the primary position in receipt should be identified. + /// + /// The is not allowed here. + public ReceiptItemType PrimaryPaymentReceiptItemType { get; set; } = ReceiptItemType.Service; + + /// + /// The named used to display in receipt. + /// When not specified, it will be automatically combined from the project's name and the word "Билет". + /// + [StringLength(64)] + public string? PrimaryPaymentReceiptName { get; set; } + + [Obsolete("Use the Discountable instead")] public bool PreferentialFeeEnabled { get; set; } = false; + + [Obsolete("Use the DiscountConditions instead")] public MarkdownString PreferentialFeeConditions { get; set; } = new MarkdownString(); + #endregion + public bool EnableCheckInModule { get; set; } = false; public bool CheckInProgress { get; set; } = false; public bool AllowSecondRoles { get; set; } = false;