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;