Project

General

Profile

Actions

Feature #972

open

Feature #961: Accounting Ledger Implementation – Patient-Based Ledger (Ticket 809)

Implement PaySplits & Family Insurance APIs in Windows Service for Accounting Ledger

Added by RishiKesh Tuniki 27 days ago. Updated 24 days ago.

Status:
Resolved
Priority:
Normal
Assignee:
Start date:
03/23/2026
Due date:
% Done:

100%

Estimated time:
Test Phase:
Select Test Phase

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

  1. Family Module Insurance API
    https://www.opendental.com/site/apifamilymodules.html
    Endpoint:
    GET /familymodules/{PatNum}/Insurance
  2. PaySplits API
    https://www.opendental.com/site/apipaysplits.html
    Endpoint:
    GET /paysplits

📦 Newly Added DTOs

  1. OpenDentalFamilyInsuranceDto
    • Represents UI-level insurance data (Primary/Secondary)
    • Combines data from PatPlan, InsSub, Carrier, and InsPlan
  2. 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

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
Actions #1

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();
    }
}

}

Actions #2

Updated by Thuan L 24 days ago

  • Status changed from New to Resolved
  • % Done changed from 0 to 100
Actions

Also available in: Atom PDF