With a bit of thought, here's my current idea - When you create an invoice, text from the item description is put into a number of tables, either directly through queries or maybe some views or rules or other SQL complexity. At least one of them has a size of 255, and the fields of printed invoices is filled from there. But some of them have a size of 50. Normally, MySQL will trim the text to put it in those size 50 fields. But for security reasons, they want/need to set the STRICT_ALL_TABLES mode on the database, and this 'Strict Mode', among other things, disallows this automated truncation.

My database doesn't have strict mode set, so it works with longer strings.

They plan to increase the size of all fields to varchar(255)when they do a major update. Personally I'd prefer that they use something like TEXT field for edited item descriptions to give us up to 64k of text where we need it, and where there is a shorter varchar() field to fill, explicitly truncate the data for that field. That together with switching from a text input to a text area HTML input would make my life a lot easier.

OK. Just finding and checking out what changed - All I can find is commit de366f - sorry, can't find how to link to it - which has only code to alter the maxlength parameter of the text entry HTML entity. In order to keep working, I altered this up to 250 in the browser, and the backend code accepted the longer string fine, and produced and emailed the invoice OK, too. Surely limiting HTML maxlength can't mitigate SQL injection...? The commit has no other changes.

After I updated to version 2.4.11, I found that the maxlength setting on the item description text box was set to 50. This is way too short to add descriptions of work done. After some time spelunking in the code I found that this was set in includes/ui/ui_lists.inc, in the function sales_items_list_cells(), line 947:

     function sales_items_list_cells($label, $name, $selected_id=null, $all_option=false, $submit_on_change=false, $editkey=false)
    {
         if ($editkey)
           set_editor('item', $name, $editkey);
    
        if ($label != null)
            echo "<td>$label</td>\n";
        echo sales_items_list($name, $selected_id, $all_option, $submit_on_change,
            '', array('cells'=>true, 'max'=>50)); // maximum is set to 50 and not default 255.
    }

So, thanks for the comment, I guess.  I set that to 'max'=>250, and I can now write invoices again - but I have to ask, why this change? And what might I have broken - so I can fix that too, because a maxlength of just 50 is unworkable.

Enter the customer payment using sales/customer payments, entering the amount instead of selecting any invoices.

Enter the refund using Banking and General Ledger/Payments, choosing 'customer' for pay to, and choose the correct customer.

Settle the credit using Sales/"Allocate Customer Payments or Credit Adjustment Notes"

You could also hack this by entering the deposit to a contra account and then entering the refund from that account; but this is the correct way that properly links it to the customer and shows up on any statements.

Sorry for the thread necromancy, but this is the google hit when you search for refunding a duplicate payment, so I wanted to add the answer.

The answer is from here: https://frontaccounting.com/fawiki/index.php?n=Help.CustomerCreditNote