Undo Is Another Change
In one line
Undoing is not an act of erasing the past but a new change that checks the current state and then executes. Separate the receipts of the original change and of the compensation, and do not overwrite a row that someone else changed later.
Why this was needed
The festival that was canceled because of the rain forecast is going ahead again. The owner asks you to undo the morning's cancellation. If you restore the whole table from a backup file, the new orders that came in after the cancellation and another owner's quantity changes can be erased too. The fact that you made a backup earlier does not by itself give permission to overwrite with it now. You have to draw the boundary again for what is fine to undo.
This assignment is a small compensation that restores a canceled order to pending. It is not an example that also undoes an actual payment cancellation or a customer notification. Refunds, shipments, and external notifications each need their own separate API and recovery policy. Here we deal with the order state and the compensation record that can fit in one PostgreSQL transaction, and within that boundary we prevent partial changes and double execution.
How it works
First, compare the original approval contents in changes with the per-row before and after versions in audit. The approval must have version 3 of order 1, and the audit must record that it changed from 3 to 4. The compensation plan does not newly approve the current table; it calculates the expected version 4 right after the cancellation from this record. If an audit row is missing or recorded a different quantity, the evidence is inconsistent, so you stop. You do not guess which change a result came from just because the current order is cancelled.
You put the customer, ID, the version right after the cancellation, the quantity, and the cancelled state all into the restore UPDATE. If they match, it changes the state to pending and increments the version one more time. If canceling version 3 made it 4, then after the compensation it is 5. If you lower it to the old version 3, old approval data can look as if it matches again. Even if you change a value back to its old appearance, the fact that a new event occurred after that must remain in the version.
원 승인 주문 1 / pending / revision 3
취소 확정 주문 1 / cancelled / revision 4
보상 확정 주문 1 / pending / revision 5
Numbers are not infinite. When you reach the end of a PostgreSQL integer, you can no longer keep incrementing for the cancellation and the compensation. If the original approval version is too large, you reject it starting from the compensation plan. In a real service you can design with a larger type or a different identification strategy, but in this lab, hiding the error and wrapping or reducing the version is not a solution.
For the compensation you leave an undo_id, the original change's original_id, and a reason. In undo_records, undo_id is unique and original_id is also unique. This is to prevent the same original change from being restored twice under different compensation IDs. A recall with the same compensation ID, original change, and reason completes with False, but if the reason or the original change is different, it is a Conflict. False is not a failure but the result that an identical request was already carried out.
The compensation receipt insert, the restore of all the orders, and the undo_audit record are one transaction. If there is a conflict on the second order, the restore of the first order and the unfinished receipt must not exist either. The original changes and audit are not modified. If you delete the original records because of the compensation, the link that could explain why the current state came to be is cut. Leaving only a new success record while the past audit is erased is not a safe compensation.
What it looks like in the field
If the program goes down right after you commit the compensation, the user does not receive the success response. If you request again with the same ID, it must read the compensation receipt and finish without a new change. If you request again with a different compensation ID, the uniqueness constraint on the original change and the content comparison tell you about the conflict. Only if you explain this difference can the owner avoid the behavior of endlessly creating new numbers just to make the error go away.
The receipt is the fact at the time of the compensation. If another owner changed or deleted the order after the compensation, it may differ from the current state. inspect_undo returns the record of that time, and reconcile_undo splits the current targets into matching, changed, and missing. Even with the same quantity, a higher version means a later change, and a new order with a different ID does not offset the missing original order. The current reconciliation only reads and does not correct anything without a new approval.
To a real customer, you communicate the cancellation receipt, the compensation receipt, and the current reconciliation result separately. If the compensation failed, you explain which premise changed and ask for a fresh judgment. This program assumes it is called by an authorized owner, and merely accepting a tenant string is not permission verification. Authentication, roles, audit retention, and access control in a real service are separate boundaries.
What you will do in the next lab
You implement in 8 steps request validation, reconciliation with the original record, single-row restore, a batch with a time limit, atomic compensation, record lookup, connection ownership, and limited retry. You actually terminate the client at three points before and after the commit and run the same and different compensation IDs concurrently. In the existing DB image, fsync and full_page_writes are turned off, so do not extend the client termination verification into a durability guarantee against a server power failure.
References: PostgreSQL UPDATE, Unique constraints, Transaction isolation. The compensation schema, reasons, and version ranges are the contract this lesson sets, and they are not a guarantee of automatic cancellation in an external system.