# SavePayment - Advanced Documentation ## Overview Creates or updates a payment record with invoice allocations, enforcing that payments cannot exceed ledger amounts and individual invoices cannot be overpaid. Validates the ledger entry is unused for new payments. Triggers claim finalization and external accounting sync (JSON upload). ## Business Rules - Caller must have the **ManagePayments** permission (validated via `ValidateCurrentPersonAndCompany`) - The payment must be associated with a valid claim - The payment must be associated with a ledger entry that is a credit (Sign = "+") - For new payments: the ledger entry must not already have an Invoiced amount > 0 (i.e., not already used on another payment) - For new payments: the ledger entry's Due amount must be > 0 - For new payments: the payment Total is set to the ledger entry's Due amount, and Invoiced is set to Due - For existing payments: the payment Total is set to the ledger entry's existing Invoiced amount - The sum of invoice payment allocations cannot exceed the payment Total - Individual invoices cannot be overpaid: the sum of all payment allocations across all payments for a given invoice cannot exceed the invoice's Total - Invoice payment amounts are rounded to two decimal places (`RoundMoney`) - The claim must belong to the current company - The claim entity is saved with `CreateEntityFinishClaimRun` for claim finalization - After saving, `spSetAmountPaidForInvoices` recalculates the paid amounts for all affected invoices (including previously associated invoices that may have been removed) - A JSON upload is triggered asynchronously for the claim after save ## Permissions & Security - Caller must be logged in with a valid company selected - Caller must be a member of the current company - Caller must have the **ManagePayments** permission - The claim must belong to the current company (firm ID check) ## Data Flow 1. Validate ManagePayments permission via `ValidateCurrentPersonAndCompany` 2. Sum all invoice payment allocations and round individual amounts 3. Load the Claim entity and verify it exists 4. Find the ledger entry within the claim's `ClaimLedgerEntries` subtype 5. Verify the claim belongs to the current firm 6. Determine payment Total based on new vs. existing payment 7. Calculate Assigned and Unassigned amounts 8. Validate invoice allocations against existing payments (`ValidatePaymentInvoices`): a. Query all existing payment-invoice records for the affected invoices b. Merge incoming allocations with existing ones (add new, update existing) c. Group by invoice and verify no invoice's total payments exceed its total 9. Validate payment business rules (`ValidatePayment`): a. Claim association required b. Ledger entry must be credit (+) c. New payments: ledger entry must be unused and have Due > 0 d. Sum of allocations cannot exceed payment Total 10. Load or create the `BackdocketPayments` entity 11. For new payments: set CreatedDate 12. Set Invoiced amount on the ledger entry 13. Set payment fields: ClaimID, ReferenceNumber, Total, Assigned, BankDate, ClaimLedgerEntryID 14. Capture existing invoice IDs before sync (to recalculate removed invoices too) 15. Sync `BackdocketPaymentInvoices` subtype with the incoming allocations 16. Begin transaction 17. Save the Claim entity (triggers `CreateEntityFinishClaimRun`) 18. Save the Payment entity 19. Merge new invoice IDs into the recalculation set 20. Execute `spSetAmountPaidForInvoices` for all affected invoices 21. Commit transaction 22. Trigger async JSON upload for the claim via `ForceJSONUploadForClaim` 23. Return the updated PaymentInfo ## Side Effects - **Database write**: BackdocketPayments record created or updated - **Database write**: BackdocketPaymentInvoices subtype synced (allocations created/updated/removed) - **Database write**: Ledger entry Invoiced amount updated on the Claim entity - **Database write**: Claim entity saved (triggers claim finalization via `CreateEntityFinishClaimRun`) - **Database update**: `spSetAmountPaidForInvoices` recalculates paid amounts for all affected invoices - **Async operation**: Claim JSON is serialized and uploaded via `ForceJSONUploadForClaim` (calls `Contact.ForceJsonUpload`) ## Error Conditions - Permission/auth errors from `ValidateCurrentPersonAndCompany` if not logged in, no company, not in company, or missing ManagePayments - `RadoloException("The {claimLabel}[{ID}] could not be found.")` - claim does not exist - `RadoloException("The ledger entry[{ID}] selected was not part of the selected {claimLabel}[{ID}].")` - ledger entry not on this claim - `RadoloException("The selected {claimLabel}[{ID}] is not part of the firm you are logged in to.")` - cross-firm access - `RadoloException("A Payment must be associated to a {claimLabel}.")` - missing claim - `RadoloException("A Payment must be associated to Ledger Entry that is a credit[+].")` - ledger entry is not income type or missing - `RadoloException("The selected ledger entry is already being used on a payment. Please select a new one")` - new payment on already-used ledger entry - `RadoloException("A payment must be greater than $0.")` - new payment on ledger entry with Due <= 0 - `RadoloException("The payment Amount cannot be less than the sum of the invoice payments.")` - allocations exceed total - `RadoloException("An invoice cannot have a negative balance. Invoice #{ID} has a negative balance of ${amount}.")` - overpayment on a specific invoice - `RadoloException("There was a problem saving this Ledger Entry : {error}")` - claim entity save failed - `RadoloException("There was a problem saving this Payment : {error}")` - payment entity save failed ## Usage Notes - The payment Total is server-controlled and derived from the ledger entry. The client-supplied Total is ignored. For new payments it comes from the ledger entry's Due; for existing payments it comes from the stored Invoiced amount. - Invoice allocations are synced as a subtype, so the input must include the complete set of allocations. Any allocations not present in the input will be removed. - The `ValidatePaymentInvoices` check queries the database for ALL existing payment allocations on the affected invoices (across all payments), not just this payment's allocations. This ensures global overpayment prevention. - Removed invoice allocations are still included in the `spSetAmountPaidForInvoices` recalculation by capturing their IDs before the sync. - The JSON upload is fire-and-forget (async without explicit error handling for the upload itself).