Feature #972
openFeature #961: Accounting Ledger Implementation – Patient-Based Ledger (Ticket 809)
Implement PaySplits & Family Insurance APIs in Windows Service for Accounting Ledger
100%
Description
We have introduced new DTOs and structure enhancements to support accurate financial and insurance data processing from OpenDental.
The following APIs need to be integrated into the Windows Service responsible for syncing data and generating the Accounting Ledger.
🔗 OpenDental API References¶
-
Family Module Insurance API
https://www.opendental.com/site/apifamilymodules.html
Endpoint:
GET /familymodules/{PatNum}/Insurance -
PaySplits API
https://www.opendental.com/site/apipaysplits.html
Endpoint:
GET /paysplits
📦 Newly Added DTOs¶
-
OpenDentalFamilyInsuranceDto
- Represents UI-level insurance data (Primary/Secondary)
- Combines data from PatPlan, InsSub, Carrier, and InsPlan
-
OpenDentalPaySplitDto*
- Represents allocation of payments
- Links payments to:
- Procedures (ProcNum)
- Payment Plans (PayPlanChargeNum)
🏗️ Master DTO Update¶
Updated:
- OpenDentalSyncDto
Added:
- List FamilyInsurances
- List PaySplits
⚙️ Implementation Requirements¶
1. Windows Service Integration
- Extend the existing OpenDental sync job to:
- Fetch data from:
- /paysplits
- /familymodules/{PatNum}/Insurance
- Map responses to the new DTOs
- Populate OpenDentalSyncDto
2. Accounting Ledger Enhancements
- Fetch data from:
Use PaySplits as the source of truth for financial allocation:
- If ProcNum > 0
- → Link payment directly to a procedure
3. Insurance Data Usage- Use FamilyInsurance API for:
- Displaying Primary / Secondary insurance
- Avoid manual joins across:
- PatPlan
- InsSub
- Carrier
- InsPlan
- Use FamilyInsurance API for:
- → Link payment directly to a procedure
Updated by RishiKesh Tuniki 27 days ago
Dto Structres Created:
/// <summary>
/// Family module insurance from GET /familymodules/{PatNum}/Insurance.
/// Combines PatPlan + InsSub + Carrier + Plan level info in one response.
/// </summary>
public List<OpenDentalFamilyInsuranceDto> FamilyInsurances { get; set; } = new();
/// <summary>
/// Payment splits from GET /paysplits.
/// Represents how payments are allocated (to procedures, plans, etc.).
/// </summary>
public List<OpenDentalPaySplitDto> PaySplits { get; set; } = new();
using System;
using System.Text.Json.Serialization;
using DentpalApiService.Dtos.Conversions;
using JsonConverter = Newtonsoft.Json.JsonConverter;
using JsonConverterAttribute = Newtonsoft.Json.JsonConverterAttribute;
namespace DentPal.Patients.PMS.OpenDental.Dtos;
///
/// DTO representing a payment split in OpenDental.
/// Matches the JSON schema returned by the OpenDental API.
///
[JsonConverter(typeof(OpenDentalPaySplitConverter))]
public class OpenDentalPaySplitDto
{
///
/// Split number (primary key).
///
[JsonPropertyName("SplitNum")]
public int SplitNum { get; set; }
/// <summary>
/// Split amount.
/// </summary>
[JsonPropertyName("SplitAmt")]
public decimal SplitAmt { get; set; }
/// <summary>
/// Patient number.
/// </summary>
[JsonPropertyName("PatNum")]
public int PatNum { get; set; }
/// <summary>
/// Payment number.
/// </summary>
[JsonPropertyName("PayNum")]
public int PayNum { get; set; }
/// <summary>
/// Provider number.
/// </summary>
[JsonPropertyName("ProvNum")]
public int ProvNum { get; set; }
/// <summary>
/// Payment plan number.
/// </summary>
[JsonPropertyName("PayPlanNum")]
public int PayPlanNum { get; set; }
/// <summary>
/// Payment date.
/// </summary>
[JsonPropertyName("DatePay")]
public DateTime DatePay { get; set; }
/// <summary>
/// Procedure number.
/// </summary>
[JsonPropertyName("ProcNum")]
public int ProcNum { get; set; }
/// <summary>
/// Entry date.
/// </summary>
[JsonPropertyName("DateEntry")]
public DateTime DateEntry { get; set; }
/// <summary>
/// Unearned type (numeric).
/// </summary>
[JsonPropertyName("UnearnedType")]
public int UnearnedType { get; set; }
/// <summary>
/// Unearned type name (text).
/// </summary>
[JsonPropertyName("unearnedType")]
public string UnearnedTypeName { get; set; }
/// <summary>
/// Clinic number.
/// </summary>
[JsonPropertyName("ClinicNum")]
public int ClinicNum { get; set; }
/// <summary>
/// Last edit timestamp.
/// </summary>
[JsonPropertyName("SecDateTEdit")]
public DateTime SecDateTEdit { get; set; }
/// <summary>
/// Adjustment number.
/// </summary>
[JsonPropertyName("AdjNum")]
public int AdjNum { get; set; }
/// <summary>
/// Payment plan charge number.
/// </summary>
[JsonPropertyName("PayPlanChargeNum")]
public int PayPlanChargeNum { get; set; }
/// <summary>
/// Payment plan debit type.
/// </summary>
[JsonPropertyName("PayPlanDebitType")]
public string PayPlanDebitType { get; set; }
}
using System.Text.Json.Serialization;
using DentpalApiService.Dtos.Conversions;
using JsonConverter = Newtonsoft.Json.JsonConverter;
using JsonConverterAttribute = Newtonsoft.Json.JsonConverterAttribute;
namespace DentPal.Patients.PMS.OpenDental.Dtos;
///
/// DTO representing Family Module Insurance from OpenDental.
/// Matches the JSON schema returned by the OpenDental API.
///
[JsonConverter(typeof(OpenDentalFamilyInsuranceConverter))]
public class OpenDentalFamilyInsuranceDto
{
[JsonPropertyName("PatNum")]
public int PatNum { get; set; }
[JsonPropertyName("InsSubNum")]
public int InsSubNum { get; set; }
[JsonPropertyName("Subscriber")]
public int Subscriber { get; set; }
[JsonPropertyName("subscriber")]
public string SubscriberName { get; set; }
[JsonPropertyName("SubscriberID")]
public string SubscriberID { get; set; }
[JsonPropertyName("SubscNote")]
public string SubscNote { get; set; }
[JsonPropertyName("PatPlanNum")]
public int PatPlanNum { get; set; }
[JsonPropertyName("Ordinal")]
public int Ordinal { get; set; }
[JsonPropertyName("ordinal")]
public string OrdinalName { get; set; }
[JsonPropertyName("IsPending")]
public string IsPending { get; set; }
[JsonPropertyName("Relationship")]
public string Relationship { get; set; }
[JsonPropertyName("PatID")]
public string PatID { get; set; }
[JsonPropertyName("CarrierNum")]
public int CarrierNum { get; set; }
[JsonPropertyName("CarrierName")]
public string CarrierName { get; set; }
[JsonPropertyName("PlanNum")]
public int PlanNum { get; set; }
[JsonPropertyName("GroupName")]
public string GroupName { get; set; }
[JsonPropertyName("GroupNum")]
public string GroupNum { get; set; }
[JsonPropertyName("PlanNote")]
public string PlanNote { get; set; }
[JsonPropertyName("FeeSched")]
public int FeeSched { get; set; }
[JsonPropertyName("feeSchedule")]
public string FeeScheduleName { get; set; }
[JsonPropertyName("PlanType")]
public string PlanType { get; set; }
[JsonPropertyName("planType")]
public string PlanTypeName { get; set; }
[JsonPropertyName("CopayFeeSched")]
public int CopayFeeSched { get; set; }
[JsonPropertyName("EmployerNum")]
public int EmployerNum { get; set; }
[JsonPropertyName("employer")]
public string EmployerName { get; set; }
[JsonPropertyName("IsMedical")]
public string IsMedical { get; set; }
}
Converters:¶
using DentPal.Patients.PMS.OpenDental.Dtos;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
namespace DentpalApiService.Dtos.Conversions
{
public class OpenDentalFamilyInsuranceConverter
: JsonConverter
{
public override OpenDentalFamilyInsuranceDto ReadJson(
JsonReader reader,
Type objectType,
OpenDentalFamilyInsuranceDto existingValue,
bool hasExistingValue,
JsonSerializer serializer)
{
var obj = JObject.Load(reader);
var dto = new OpenDentalFamilyInsuranceDto();
// ---- Ordinal (numeric) ----
if (obj.TryGetValue("Ordinal", StringComparison.Ordinal, out var ordinalNum) &&
ordinalNum.Type == JTokenType.Integer)
{
dto.Ordinal = ordinalNum.Value<int>();
}
// ---- ordinal (text) ----
if (obj.TryGetValue("ordinal", StringComparison.Ordinal, out var ordinalText) &&
ordinalText.Type == JTokenType.String)
{
dto.OrdinalName = ordinalText.Value<string>();
}
// ---- FeeSched / feeSchedule ----
if (obj.TryGetValue("FeeSched", StringComparison.Ordinal, out var feeNum) &&
feeNum.Type == JTokenType.Integer)
{
dto.FeeSched = feeNum.Value<int>();
}
if (obj.TryGetValue("feeSchedule", StringComparison.Ordinal, out var feeText) &&
feeText.Type == JTokenType.String)
{
dto.FeeScheduleName = feeText.Value<string>();
}
// ---- PlanType / planType ----
if (obj.TryGetValue("PlanType", StringComparison.Ordinal, out var planType) &&
planType.Type == JTokenType.String)
{
dto.PlanType = planType.Value<string>();
}
if (obj.TryGetValue("planType", StringComparison.Ordinal, out var planTypeText) &&
planTypeText.Type == JTokenType.String)
{
dto.PlanTypeName = planTypeText.Value<string>();
}
// Remove duplicates to avoid collision
obj.Remove("Ordinal");
obj.Remove("ordinal");
obj.Remove("FeeSched");
obj.Remove("feeSchedule");
obj.Remove("PlanType");
obj.Remove("planType");
serializer.Populate(obj.CreateReader(), dto);
return dto;
}
public override void WriteJson(
JsonWriter writer,
OpenDentalFamilyInsuranceDto value,
JsonSerializer serializer)
{
writer.WriteStartObject();
// Ordinal
writer.WritePropertyName("Ordinal");
writer.WriteValue(value.Ordinal);
if (!string.IsNullOrEmpty(value.OrdinalName))
{
writer.WritePropertyName("ordinal");
writer.WriteValue(value.OrdinalName);
}
// FeeSched
writer.WritePropertyName("FeeSched");
writer.WriteValue(value.FeeSched);
if (!string.IsNullOrEmpty(value.FeeScheduleName))
{
writer.WritePropertyName("feeSchedule");
writer.WriteValue(value.FeeScheduleName);
}
// PlanType
if (!string.IsNullOrEmpty(value.PlanType))
{
writer.WritePropertyName("PlanType");
writer.WriteValue(value.PlanType);
}
if (!string.IsNullOrEmpty(value.PlanTypeName))
{
writer.WritePropertyName("planType");
writer.WriteValue(value.PlanTypeName);
}
serializer.Serialize(writer, value);
writer.WriteEndObject();
}
}
}
using DentPal.Patients.PMS.OpenDental.Dtos;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
namespace DentpalApiService.Dtos.Conversions
{
public class OpenDentalPaySplitConverter
: JsonConverter
{
public override OpenDentalPaySplitDto ReadJson(
JsonReader reader,
Type objectType,
OpenDentalPaySplitDto existingValue,
bool hasExistingValue,
JsonSerializer serializer)
{
var obj = JObject.Load(reader);
var dto = new OpenDentalPaySplitDto();
// ---- UnearnedType (numeric) ----
if (obj.TryGetValue("UnearnedType", StringComparison.Ordinal, out var typeNum) &&
typeNum.Type == JTokenType.Integer)
{
dto.UnearnedType = typeNum.Value<int>();
}
// ---- unearnedType (text) ----
if (obj.TryGetValue("unearnedType", StringComparison.Ordinal, out var typeText) &&
typeText.Type == JTokenType.String)
{
dto.UnearnedTypeName = typeText.Value<string>();
}
// Remove both to avoid collision
obj.Remove("UnearnedType");
obj.Remove("unearnedType");
// Populate remaining properties
serializer.Populate(obj.CreateReader(), dto);
return dto;
}
public override void WriteJson(
JsonWriter writer,
OpenDentalPaySplitDto value,
JsonSerializer serializer)
{
writer.WriteStartObject();
writer.WritePropertyName("UnearnedType");
writer.WriteValue(value.UnearnedType);
if (!string.IsNullOrEmpty(value.UnearnedTypeName))
{
writer.WritePropertyName("unearnedType");
writer.WriteValue(value.UnearnedTypeName);
}
serializer.Serialize(writer, value);
writer.WriteEndObject();
}
}
}